package com.foundation.canal.client;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.foundation.canal.util.CanalThreadPoolExecute;
import com.foundation.canal.config.CanalConfig;
import com.foundation.canal.config.SingleCanalConfig;
import com.foundation.canal.config.ZkClusterCanalConfig;
import com.foundation.canal.invoke.ICanalInvoke;
import com.foundation.canal.config.SocketsClusterCanalConfig;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;

/**
 * @author fqh
 * @version 1.0  2016/9/26
 */
public class CustomSimpleCanalClient implements ApplicationContextAware{

    private static final Logger logger = LoggerFactory.getLogger(CustomSimpleCanalClient.class);

    private ApplicationContext applicationContext;

    private CanalConnector connector;

    private ICanalInvoke globalInvoke;

    private Map<String, List<String>> invokeMapIOC;

    private Map<String, List<ICanalInvoke>> invokeMap;
    private LinkedBlockingQueue<Entry> customTableQueue = new LinkedBlockingQueue<Entry>();
    private CanalConfig canalConfig;
    private static final ExecutorService threadPool =new CanalThreadPoolExecute(3,3,0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
    @PostConstruct
    public void init() {
        initInvoke();
        initConector();
        startInvoke();
        startClient();
    }

    @PreDestroy
    public void  destroy(){
        threadPool.shutdownNow();
        if(connector!=null){
            connector.disconnect();
        }

    }

    public void initInvoke(){
        if(this.invokeMapIOC==null || this.invokeMapIOC.isEmpty()){
            return;
        }
        invokeMap = Maps.newConcurrentMap();
        for (Map.Entry<String, List<String>> entry : invokeMapIOC.entrySet()) {
            for (String beanId : entry.getValue()) {
                ICanalInvoke tableInvoke = (ICanalInvoke)applicationContext.getBean(beanId,ICanalInvoke.class);
                List<ICanalInvoke> canalInvokeList = invokeMap.get(entry.getKey());
                if(canalInvokeList == null){
                    canalInvokeList = new ArrayList<ICanalInvoke>();
                    invokeMap.put(entry.getKey(),canalInvokeList);
                }
                canalInvokeList.add(tableInvoke);
            }
        }
    }

    public void initConector() {
        if (canalConfig == null) {
            throw new IllegalArgumentException("CustomSimpleCanalClient canalConfig property is empty!");
        }
        if ((invokeMap == null || invokeMap.isEmpty())&& this.globalInvoke==null) {
            throw new IllegalArgumentException("CustomSimpleCanalClient invokeMap property is empty!");
        }
        if (canalConfig instanceof SingleCanalConfig) {
            String socketStr = ((SingleCanalConfig) canalConfig).getSocketAddress();
            logger.info("canal will connection to {}.", socketStr);
            connector = CanalConnectors.newSingleConnector(this.getSocketAddressByString(socketStr),
                    canalConfig.getDestination(), canalConfig.getUsername(), canalConfig.getPassword());
        } else if (canalConfig instanceof SocketsClusterCanalConfig) {
            List<SocketAddress> socketAddressList = new ArrayList<SocketAddress>();
            for (String sok : ((SocketsClusterCanalConfig) canalConfig).getSocketAddress()) {
                logger.info("canal will connection to {}.", sok);
                socketAddressList.add(this.getSocketAddressByString(sok));
            }
            connector = CanalConnectors.newClusterConnector(socketAddressList,
                    canalConfig.getDestination(), canalConfig.getUsername(), canalConfig.getPassword());
        } else if (canalConfig instanceof ZkClusterCanalConfig) {
            String zkAddress = ((ZkClusterCanalConfig) canalConfig).getZkAddress();
            logger.info("canal will connection to {}.", zkAddress);
            connector = CanalConnectors.newClusterConnector(zkAddress,
                    canalConfig.getDestination(), canalConfig.getUsername(), canalConfig.getPassword());
        }
        connector.connect();
        connector.subscribe(canalConfig.getSubscribeChannel());
        connector.rollback();
    }

    /**
     * 启动canal client服务
     * 采用异步线程方式生产数据到内存队列
     */
    public void startClient() {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                while (!threadPool.isShutdown() && !threadPool.isTerminated()) {
                    Message message = connector.getWithoutAck(canalConfig.getFetchSize(),10L,TimeUnit.SECONDS); // 获取指定数量的数据
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            logger.warn("CustomSimpleCanalClient invoke thread interrupted!");
                        }
                    } else {
                        List<Entry> entryList = message.getEntries();
                        for (Entry entry : entryList) {
                            try {
                                customTableQueue.put(entry);
                            } catch (InterruptedException e) {
                                //do nothing
                                logger.warn("customTableQueue interrupt.", e);
                            }
                        }
                    }
                    connector.ack(batchId); // 提交确认
                }
            }
        });
    }

    /**
     * 启动内存队列消费线程
     */
    public void startInvoke() {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                Entry entry = null;
                do {
                    try {
                        entry = customTableQueue.take();
                    } catch (InterruptedException e) {
                        //do nothing
                        logger.warn("customTableQueue interrupt.", e);
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e1) {
                        }
                        continue;
                    }
                    if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                        continue;
                    }

                    RowChange rowChage = null;
                    try {
                        rowChage = RowChange.parseFrom(entry.getStoreValue());
                    } catch (Exception e) {
                        logger.warn("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
                        continue;
                    }

                    EventType eventType = rowChage.getEventType();
                    Header header = entry.getHeader();
                    logger.info(String.format("binlog[%s:%s] , name[%s,%s] , eventType : %s",
                            header.getLogfileName(), header.getLogfileOffset(),
                            header.getSchemaName(), header.getTableName(),
                            eventType));
                    //
                    for (RowData rowData : rowChage.getRowDatasList()) {
                        String key = this.getMappingKey(header.getSchemaName(), header.getTableName());
                        //调用全局canal invoke
                        invokeMethod(globalInvoke,eventType,rowData);
                        if(invokeMap==null || invokeMap.isEmpty()){
                            continue;
                        }
                        List<ICanalInvoke> invokeList = invokeMap.get(key);
                        if (invokeList != null && !invokeList.isEmpty()) {
                            for (ICanalInvoke iCanalInvoke : invokeList) {
                                invokeMethod(iCanalInvoke,eventType,rowData);
                            }
                        }
                    }
                } while (!threadPool.isShutdown() && !threadPool.isTerminated());

            }

            /**
             * 反射调用对应处理方法(删除,新增,更新)
             * @param callback
             * @param eventType
             * @param rowData
             */
            private void invokeMethod(ICanalInvoke callback,EventType eventType, RowData rowData) {
                if (callback != null) {
                    try {
                        if (eventType == EventType.DELETE) {
                            callback.onDelete(rowData);
                        } else if (eventType == EventType.INSERT) {
                            callback.onInsert(rowData);
                        } else if (eventType == EventType.UPDATE){
                            callback.onUpdate(rowData);
                        }else{
                            logger.warn("not support eventType:{}",eventType.toString());
                        }
                    } catch (Exception e) {
                        logger.error("invoke method fail!",e);
                    }
                }
            }

            private String getMappingKey(String database, String tableName) {
                return String.format("%s.%s", database, tableName).toLowerCase();
            }
        });
    }


    private SocketAddress getSocketAddressByString(String socketStr) {
        String serverUrl = socketStr.substring(0, socketStr.lastIndexOf(":"));
        String port = socketStr.substring(socketStr.lastIndexOf(":") + 1);
        return new InetSocketAddress(serverUrl, Integer.valueOf(port));
    }

    public void setInvokeMapIOC(Map<String, List<String>> invokeMapIOC) {
        Set<Map.Entry<String, List<String>>> entrySet = invokeMapIOC.entrySet();
        this.invokeMapIOC =Maps.newConcurrentMap();
        for (Map.Entry<String, List<String>> entry : entrySet) {
            this.invokeMapIOC.put(entry.getKey().toLowerCase(), entry.getValue());
        }
    }

    public void setCanalConfig(CanalConfig canalConfig) {
        this.canalConfig = canalConfig;
    }

    public void setGlobalInvoke(ICanalInvoke globalInvoke) {
        this.globalInvoke = globalInvoke;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}