package cn.quantgroup.xyqb.thirdparty.jcaptcha;

import cn.quantgroup.xyqb.config.captcha.RedisCaptchaStore;
import com.octo.captcha.Captcha;
import com.octo.captcha.engine.CaptchaEngine;
import com.octo.captcha.service.AbstractCaptchaService;
import com.octo.captcha.service.AbstractManageableCaptchaServiceMBean;
import com.octo.captcha.service.CaptchaService;
import com.octo.captcha.service.CaptchaServiceException;
import com.octo.captcha.service.captchastore.CaptchaStore;
import org.apache.commons.collections.FastHashMap;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;

/**
 * 类名称：AbstractManageableCaptchaService
 * 类描述：
 *
 * @author 李宁
 * @version 1.0.0
 * 创建时间：15/12/25 13:48
 * 修改人：
 * 修改时间：15/12/25 13:48
 * 修改备注：
 */
public abstract class AbstractManageableCaptchaService extends AbstractCaptchaService implements AbstractManageableCaptchaServiceMBean, CaptchaService {

    private int minGuarantedStorageDelayInSeconds;
    private int captchaStoreMaxSize;
    private int captchaStoreSizeBeforeGarbageCollection;
    private int numberOfGeneratedCaptchas;
    private int numberOfCorrectResponse;
    private int numberOfUncorrectResponse;
    private int numberOfGarbageCollectedCaptcha;
    private FastHashMap times;
    private long oldestCaptcha;

    protected AbstractManageableCaptchaService(CaptchaStore captchaStore, CaptchaEngine captchaEngine, int minGuarantedStorageDelayInSeconds, int maxCaptchaStoreSize) {
        super(captchaStore, captchaEngine);
        this.numberOfGeneratedCaptchas = 0;
        this.numberOfCorrectResponse = 0;
        this.numberOfUncorrectResponse = 0;
        this.numberOfGarbageCollectedCaptcha = 0;
        this.oldestCaptcha = 0L;
        this.setCaptchaStoreMaxSize(maxCaptchaStoreSize);
        this.setMinGuarantedStorageDelayInSeconds(minGuarantedStorageDelayInSeconds);
        this.setCaptchaStoreSizeBeforeGarbageCollection((int) Math.round(0.8D * (double) maxCaptchaStoreSize));
        this.times = new FastHashMap();
    }

    protected AbstractManageableCaptchaService(CaptchaStore captchaStore, CaptchaEngine captchaEngine, int minGuarantedStorageDelayInSeconds, int maxCaptchaStoreSize, int captchaStoreLoadBeforeGarbageCollection) {
        this(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds, maxCaptchaStoreSize);
        if (maxCaptchaStoreSize < captchaStoreLoadBeforeGarbageCollection) {
            throw new IllegalArgumentException("the max store size can\'t be less than garbage collection size. if you want to disable garbage collection (this is not recommended) you may set them equals (max=garbage)");
        } else {
            this.setCaptchaStoreSizeBeforeGarbageCollection(captchaStoreLoadBeforeGarbageCollection);
        }
    }

    public String getCaptchaEngineClass() {
        return this.engine.getClass().getName();
    }

    public void setCaptchaEngineClass(String theClassName) throws IllegalArgumentException {
        try {
            Object e = Class.forName(theClassName).newInstance();
            if (e instanceof CaptchaEngine) {
                this.engine = (CaptchaEngine) e;
            } else {
                throw new IllegalArgumentException("Class is not instance of CaptchaEngine! " + theClassName);
            }
        } catch (InstantiationException | IllegalAccessException | RuntimeException | ClassNotFoundException var) {
            throw new IllegalArgumentException(var.getMessage());
        }
    }

    public CaptchaEngine getEngine() {
        return this.engine;
    }

    public void setCaptchaEngine(CaptchaEngine engine) {
        this.engine = engine;
    }

    public int getMinGuarantedStorageDelayInSeconds() {
        return this.minGuarantedStorageDelayInSeconds;
    }

    public void setMinGuarantedStorageDelayInSeconds(int theMinGuarantedStorageDelayInSeconds) {
        this.minGuarantedStorageDelayInSeconds = theMinGuarantedStorageDelayInSeconds;
    }

    public long getNumberOfGeneratedCaptchas() {
        return (long) this.numberOfGeneratedCaptchas;
    }

    public void setNumberOfGeneratedCaptchas(int numberOfGeneratedCaptchas) {
        this.numberOfGeneratedCaptchas = numberOfGeneratedCaptchas;
    }

    public long getNumberOfCorrectResponses() {
        return (long) this.numberOfCorrectResponse;
    }

    public long getNumberOfUncorrectResponses() {
        return (long) this.numberOfUncorrectResponse;
    }

    public int getCaptchaStoreSize() {
//        return this.store.getSize();
        return this.times.size();
    }

    public int getNumberOfGarbageCollectableCaptchas() {
        return this.getGarbageCollectableCaptchaIds(System.currentTimeMillis()).size();
    }

    public long getNumberOfGarbageCollectedCaptcha() {
        return (long) this.numberOfGarbageCollectedCaptcha;
    }

    public void setNumberOfGarbageCollectedCaptcha(int numberOfGarbageCollectedCaptcha) {
        this.numberOfGarbageCollectedCaptcha = numberOfGarbageCollectedCaptcha;
    }

    public int getCaptchaStoreSizeBeforeGarbageCollection() {
        return this.captchaStoreSizeBeforeGarbageCollection;
    }

    public void setCaptchaStoreSizeBeforeGarbageCollection(int captchaStoreSizeBeforeGarbageCollection) {
        if (this.captchaStoreMaxSize < captchaStoreSizeBeforeGarbageCollection) {
            throw new IllegalArgumentException("the max store size can\'t be less than garbage collection size. if you want to disable garbage collection (this is not recommended) you may set them equals (max=garbage)");
        } else {
            this.captchaStoreSizeBeforeGarbageCollection = captchaStoreSizeBeforeGarbageCollection;
        }
    }

    public int getCaptchaStoreMaxSize() {
        return this.captchaStoreMaxSize;
    }

    public void setCaptchaStoreMaxSize(int size) {
        if (size < this.captchaStoreSizeBeforeGarbageCollection) {
            throw new IllegalArgumentException("the max store size can\'t be less than garbage collection size. if you want to disable garbage collection (this is not recommended) you may set them equals (max=garbage)");
        } else {
            this.captchaStoreMaxSize = size;
        }
    }

    public void garbageCollectCaptchaStore() {
        long now = System.currentTimeMillis();
        // id 垃圾回收
        Collection garbageCollectableCaptchaIds = this.getGarbageCollectableCaptchaIds(now);
        // 存储器垃圾回收
        this.garbageCollectCaptchaStore(garbageCollectableCaptchaIds.iterator());
    }

    private void garbageCollectCaptchaStore(Iterator garbageCollectableCaptchaIds) {
        long now = System.currentTimeMillis();
        long limit = now - (long) (1000 * this.minGuarantedStorageDelayInSeconds);

        while (garbageCollectableCaptchaIds.hasNext()) {
            String id = garbageCollectableCaptchaIds.next().toString();
            if ((Long) this.times.get(id) < limit) {
                // 清理计数器
                this.times.remove(id);
                // 对于 redis 存储, 使用其自动失效机制, 不手动清理
                if (this.store.getClass() != RedisCaptchaStore.class) {
                    this.store.removeCaptcha(id);
                }
                ++this.numberOfGarbageCollectedCaptcha;
            }
        }

    }

    private Collection getGarbageCollectableCaptchaIds(long now) {
        HashSet garbageCollectableCaptchas = new HashSet();
        long limit = now - (long) (1000 * this.getMinGuarantedStorageDelayInSeconds());
        if (limit > this.oldestCaptcha) {
            for (Object o : this.times.keySet()) {
                String id = (String) o;
                long captchaDate = (Long) this.times.get(id);
                this.oldestCaptcha = Math.min(captchaDate, this.oldestCaptcha == 0L ? captchaDate : this.oldestCaptcha);
                if (captchaDate < limit) {
                    garbageCollectableCaptchas.add(id);
                }
            }
        }

        return garbageCollectableCaptchas;
    }

    public void emptyCaptchaStore() {
        this.store.empty();
        this.times = new FastHashMap();
    }

    protected Captcha generateAndStoreCaptcha(Locale locale, String ID) {
        if (this.isCaptchaStoreFull()) {
            logger.info("生成图形验证码时检测到, 容量已满, 执行垃圾回收");
            long now = System.currentTimeMillis();
            Collection garbageCollectableCaptchaIds = this.getGarbageCollectableCaptchaIds(now);
            if (garbageCollectableCaptchaIds.size() > 0) {
                this.garbageCollectCaptchaStore(garbageCollectableCaptchaIds.iterator());
                return this.generateAndStoreCaptcha(locale, ID);
            } else {
                logger.error("Captcha Store 已满, 但无可回收验证码, 请检查验证码超时配置和验证码临界值!");
                throw new CaptchaServiceException("Store is full, try to increase CaptchaStore Size orto decrease time out, or to decrease CaptchaStoreSizeBeforeGrbageCollection");
            }
        } else {
            if (this.isCaptchaStoreQuotaReached()) {
                logger.info("生成图形验证码时检测到, 容量已达配额, 执行垃圾回收");
                this.garbageCollectCaptchaStore();
            }

            return this.generateCountTimeStampAndStoreCaptcha(ID, locale);
        }
    }

    private Captcha generateCountTimeStampAndStoreCaptcha(String ID, Locale locale) {
        ++this.numberOfGeneratedCaptchas;
        Long now = System.currentTimeMillis();
        this.times.put(ID, now);
        return super.generateAndStoreCaptcha(locale, ID);
    }

    private boolean isCaptchaStoreFull() {
        return this.getCaptchaStoreMaxSize() != 0 && this.getCaptchaStoreSize() >= this.getCaptchaStoreMaxSize();
    }

    private boolean isCaptchaStoreQuotaReached() {
        return this.getCaptchaStoreSize() >= this.getCaptchaStoreSizeBeforeGarbageCollection();
    }

    public Boolean validateResponseForID(String ID, Object response) throws CaptchaServiceException {
        Boolean valid = super.validateResponseForID(ID, response);
        this.times.remove(ID);
        if (valid) {
            ++this.numberOfCorrectResponse;
        } else {
            ++this.numberOfUncorrectResponse;
        }

        return valid;
    }

    public int getNumberOfCorrectResponse() {
        return numberOfCorrectResponse;
    }

    public void setNumberOfCorrectResponse(int numberOfCorrectResponse) {
        this.numberOfCorrectResponse = numberOfCorrectResponse;
    }

    public int getNumberOfUncorrectResponse() {
        return numberOfUncorrectResponse;
    }

    public void setNumberOfUncorrectResponse(int numberOfUncorrectResponse) {
        this.numberOfUncorrectResponse = numberOfUncorrectResponse;
    }

    public void addNumberOfGarbageCollectedCaptcha(int num) {
        this.numberOfGarbageCollectedCaptcha += num;
    }

    protected void addNumberOfUncorrectResponse(int num) {
        this.numberOfUncorrectResponse += num;
    }

    protected void addNumberOfCorrectResponse(int num) {
        this.numberOfCorrectResponse += num;
    }

    public FastHashMap getTimes() {
        return times;
    }

    public void setTimes(FastHashMap times) {
        this.times = times;
    }

    public long getOldestCaptcha() {
        return oldestCaptcha;
    }

    public void setOldestCaptcha(long oldestCaptcha) {
        this.oldestCaptcha = oldestCaptcha;
    }
}
