package com.bsmlab.dfx.agent.config.datasource;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class RefreshableSqlSessionFactoryBean extends SqlSessionFactoryBean implements DisposableBean {
    private SqlSessionFactory proxy;
    private int interval = 500;
    private Timer timer;
    private TimerTask task;
    private Resource[] mapperLocations;
    private boolean running = false;
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    @Override
    public void setMapperLocations(Resource[] mapperLocations) {
        super.setMapperLocations(mapperLocations);
        this.mapperLocations = mapperLocations;
    }

    public void setInterval(int interval) {
        this.interval = interval;
    }

    public void refresh() throws Exception {
        if(log.isInfoEnabled()) {
            log.info("> Refreshing SQL Mapper Configuration...");
        }
        w.lock();
        try {
            super.afterPropertiesSet();
            // refresh()가 호출될 때 기존 proxy를 재생성하여 기존 세션과 충돌을 방지한다.
            proxy = (SqlSessionFactory) Proxy.newProxyInstance(
                    SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSessionFactory.class}
                    , (proxy, method, args) -> method.invoke(getParentObject(), args)
            );
        } finally {
            w.unlock();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();
        setRefreshable();
    }

    private void setRefreshable() {
        proxy = (SqlSessionFactory) Proxy.newProxyInstance(
                SqlSessionFactory.class.getClassLoader(),
                new Class[]{SqlSessionFactory.class},
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        return method.invoke(getParentObject(), args);
                    }
                });

        task = new TimerTask() {
            private Map<Resource, Long> map = new HashMap<>();

            public void run() {
                if(isModified()) {
                    try {
                        refresh();
                    } catch(Exception e) {
                        log.error("caught exception", e);
                    }
                }
            }

            private boolean isModified() {
                boolean retVal = false;
                if(mapperLocations != null) {
                    for(int i = 0; i < mapperLocations.length; i++) {
                        Resource mappingLocation = mapperLocations[i];
                        retVal |= findModifiedResource(mappingLocation);
                    }
                }
                return retVal;
            }

            private boolean findModifiedResource(Resource resource) {
                boolean retVal = false;
                List<String> modifiedResources = new ArrayList<>();
                try {
                    long modified = resource.lastModified();

                    if(map.containsKey(resource)) {
                        long lastModified = ((Long) map.get(resource)) .longValue();

                        if(lastModified != modified) {
                            map.put(resource, modified);

                            //modifiedResources.add(resource.getDescription());	// 전체경로 
                            modifiedResources.add(resource.getFilename());		// 파일명							

                            retVal = true;
                        }
                    } else {
                        map.put(resource, modified);
                    }
                } catch (IOException e) {
                    log.error("caught exception", e);
                }
                if(retVal) {
                    if(log.isInfoEnabled()) {
                        log.info("======================================================================================");
                        log.info("> Update File name : " + modifiedResources);
                    }
                }
                return retVal;
            }
        };

        timer = new Timer(true);
        resetInterval();
    }

    private Object getParentObject() throws Exception {
        r.lock();
        try {
            return super.getObject();
        } finally {
            r.unlock();
        }
    }

    @Override
    public SqlSessionFactory getObject() {
        return this.proxy;
    }

    @Override
    public Class<? extends SqlSessionFactory> getObjectType() {
        return (this.proxy != null ? this.proxy.getClass() : SqlSessionFactory.class);
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public void setCheckInterval(int ms) {
        interval = ms;
        if(timer != null) {
            resetInterval();
        }
    }

    private void resetInterval() {
        if(running) {
            timer.cancel();

            running = false;
        }

        if(interval > 0) {
            timer.schedule(task, 0, interval); running = true;
        }
    }

    @Override
    public void destroy() throws Exception {
        timer.cancel();
    }
}
