package me.chyxion.tigon.mybatis;

import java.io.File;
import java.net.URL;
import java.util.List;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import org.w3c.dom.Document;
import javax.xml.transform.*;
import org.w3c.dom.DocumentType;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import javax.xml.transform.dom.DOMSource;
import org.springframework.util.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.Resource;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import javax.xml.transform.stream.StreamResult;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.ByteArrayResource;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

/**
 * SqlSessionFactoryBean Extended, 
 * Add Interceptor,
 * Register __tigon_mybatis__.xml
 * @version 0.0.1
 * @author Shaun Chyxion <br>
 * chyxion@163.com <br>
 * Jul 12, 2014 3:36:13 PM
 */
@Slf4j
public class TigonSqlSessionFactoryBean extends SqlSessionFactoryBean {
    private static final List<MapperXmlProcessor> MAPPER_XML_PROCESSORS =
        new LinkedList<MapperXmlProcessor>();
    static {
        MAPPER_XML_PROCESSORS.add(new TableXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new PrimaryKeyXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new ColsXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new InsertXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new ExistsXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new FindXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new ListXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new CountXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new UpdateXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new SetNullXmlProcessor());
        MAPPER_XML_PROCESSORS.add(new DeleteXmlProcessor());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setMapperLocations(final Resource[] mapperLocations) {
        if (mapperLocations != null && mapperLocations.length > 0) {
            final XMLMapperEntityResolver xmlMapperEntityResolver =
                new XMLMapperEntityResolver();
            final List<Resource> updatedMapperLocations =
                new ArrayList<Resource>(mapperLocations.length);
            // update mapper xml in thread
            for (final Resource mapperLocation : mapperLocations) {
                updatedMapperLocations.add(updateMapperXml(xmlMapperEntityResolver, mapperLocation));
            }
            super.setMapperLocations(updatedMapperLocations.toArray(mapperLocations));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        SqlSessionFactory ssf = super.buildSqlSessionFactory();
        Configuration config = ssf.getConfiguration();
        config.setMapUnderscoreToCamelCase(true);
        config.addInterceptor(new KeyGenInterceptor());
        // tigon mapper
        Resource mapperLocation =
            findResource("classpath*:__tigon_mybatis__.xml");
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            config, mapperLocation.toString(), config.getSqlFragments());
        xmlMapperBuilder.parse();
        return ssf;
    }

    // --
    // private methods

    /**
     * find resource by path
     * @param path path
     * @return resource
     */
    private Resource findResource(String path) {
        Resource resource = null;
        Resource[] resources = null;
        try {
            resources = new PathMatchingResourcePatternResolver()
                .getResources(path);
        }
        catch (Exception e) {
            throw new IllegalStateException(
                "Find Resource [" + path + "] Error Caused", e);
        }
        if (resources.length == 0) {
            throw new IllegalStateException(
                "No Resource [" + path + "] Found");
        }
        else if (resources.length == 1) {
            resource = resources[0];
            log.info("Resource [{}] Of Path [{}] Found.", resource, path);
        }
        else {
            throw new IllegalStateException("Multiple Resources ["
                    + org.apache.commons.lang3.StringUtils.join(resources, ", ")
                    + "] Of Path [" + path + "] Found");
        }
        return resource;
    }

    @SuppressWarnings("unchecked")
    private Class<BaseMapper<?, ?>> getMapperClass(String name) {
        Class<BaseMapper<?, ?>> mapperClassRtn = null;
        try {
            Class<?> classFound = ClassUtils.forName(name,
                ClassUtils.getDefaultClassLoader());
            if (BaseMapper.class.isAssignableFrom(classFound)) {
                mapperClassRtn = (Class<BaseMapper<?, ?>>) classFound;
            }
        }
        catch (ClassNotFoundException e) {
            log.warn("No Mapper Class [{}] Found.", name);
        }
        return mapperClassRtn;
    }

    private Transformer newTransformer() {
        try {
            return TransformerFactory.newInstance().newTransformer();
        }
        catch (TransformerConfigurationException e) {
            throw new IllegalStateException(
                    "Create XML Transformer Error Caused", e);
        }
    }

    private Resource updateMapperXml(
        final XMLMapperEntityResolver xmlMapperEntityResolver,
        final Resource mapperLocation) {

        InputStream inputStream = null;
        try {
            Resource mapperResourceUpdated = null;
            inputStream = mapperLocation.getInputStream();
            final XPathParser xPathParser =
                new XPathParser(inputStream, false, null, xmlMapperEntityResolver);
            final String mapperNamespace =
                xPathParser.evalString("/mapper/@namespace");

            if (StringUtils.isNotBlank(mapperNamespace)) {
                final Class<BaseMapper<?, ?>> mapperClass =
                    getMapperClass(mapperNamespace);
                if (mapperClass != null) {
                    MetaObject metaXpp = SystemMetaObject.forObject(xPathParser);
                    Document doc = (Document) metaXpp.getValue("document");
                    boolean updated = false;
                    for (MapperXmlProcessor mapperXmlProcessor : MAPPER_XML_PROCESSORS) {
                        if (mapperXmlProcessor.accept(mapperClass, xPathParser)) {
                            mapperXmlProcessor.process(mapperClass, doc);
                            updated = true;
                        }
                    }
                    if (updated) {
                        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        final DocumentType docType = doc.getDoctype();
                        final Transformer transformer = newTransformer();
                        transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
                            docType.getPublicId());
                        transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
                            docType.getSystemId());
                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                        transformer.transform(new DOMSource(doc), new StreamResult(baos));
                        mapperResourceUpdated = new TigonMapperResource(baos.toByteArray(), mapperLocation);
                        log.info("Tigon MyBatis Mapper [{}] Updated Result [\n{}].", mapperResourceUpdated, baos);
                    }
                }
            }
            return mapperResourceUpdated != null ? mapperResourceUpdated : mapperLocation;
        }
        catch (IOException e) {
            throw new IllegalStateException(
                    "Read MyBatis Mapper Resource [" + mapperLocation + "] Error Caused", e);
        }
        catch (TransformerException e) {
            throw new IllegalStateException(
                    "Transform MyBatis Mapper Resource [" + mapperLocation + "] Error Caused", e);
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    log.warn("Close MyBatis Mapper Resource [{}] Input Stream Error Caused.",
                            mapperLocation, e);
                }
            }
        }
    }

    static class TigonMapperResource extends ByteArrayResource {
        private final Resource originResource;

        public TigonMapperResource(byte[] byteArray, Resource originResource) {
            super(byteArray);
            this.originResource = originResource;
        }

        /**
         * {@inheritDoc}
         * for JRebel monitor file
         */
        @Override
        public URL getURL() throws IOException {
            return originResource.getURL();
        }

        /**
         * {@inheritDoc}
         * for JRebel monitor file
         */
        @Override
        public File getFile() throws IOException {
            return originResource.getFile();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getFilename() {
            return originResource.getFilename();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getDescription() {
            return "[TIGON] " + originResource.getDescription();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            return "[TIGON] " + originResource.toString();
        }
    }
}
