/*
 * Copyright 1999-2011 Alibaba Group.
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.dubbo.monitor.simple;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import com.alibaba.dubbo.common.utils.ConfigUtils;
import com.alibaba.dubbo.common.utils.NamedThreadFactory;
import com.alibaba.dubbo.common.utils.NetUtils;
import com.alibaba.dubbo.monitor.MonitorService;

/**
 * SimpleMonitorService
 * 
 * @author william.liangf
 */
public class SimpleMonitorService implements MonitorService {

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

    private static final String[] types = {SUCCESS, FAILURE, ELAPSED, CONCURRENT, MAX_ELAPSED, MAX_CONCURRENT};
    
    private static final String POISON_PROTOCOL = "poison";
    
    // 定时任务执行器
    private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboMonitorTimer", true));

    // 图表绘制定时器
    private final ScheduledFuture<?> chartFuture;

    private final Thread writeThread;
    
    private final BlockingQueue<URL> queue;
    
    private String statisticsDirectory = "statistics";

    private String chartsDirectory = "charts";
    
    private volatile boolean running = true;
    
    private static SimpleMonitorService INSTANCE = null;

    public static SimpleMonitorService getInstance() {
        return INSTANCE;
    }

    public String getStatisticsDirectory() {
        return statisticsDirectory;
    }
    
    public void setStatisticsDirectory(String statistics) {
        if (statistics != null) {
            this.statisticsDirectory = statistics;
        }
    }

    public String getChartsDirectory() {
        return chartsDirectory;
    }

    public void setChartsDirectory(String charts) {
        if (charts != null) {
            this.chartsDirectory = charts;
        }
    }
    
    public SimpleMonitorService() {
        queue = new LinkedBlockingQueue<URL>(Integer.parseInt(ConfigUtils.getProperty("dubbo.monitor.queue", "100000")));
        writeThread = new Thread(new Runnable() {
            public void run() {
                while (running) {
                    try {
                        write(); // 记录统计日志
                    } catch (Throwable t) { // 防御性容错
                        logger.error("Unexpected error occur at write stat log, cause: " + t.getMessage(), t);
                        try {
                            Thread.sleep(5000); // 失败延迟
                        } catch (Throwable t2) {
                        }
                    }
                }
            }
        });
        writeThread.setDaemon(true);
        writeThread.setName("DubboMonitorAsyncWriteLogThread");
        writeThread.start();
        chartFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    draw(); // 绘制图表
                } catch (Throwable t) { // 防御性容错
                    logger.error("Unexpected error occur at draw stat chart, cause: " + t.getMessage(), t);
                }
            }
        }, 1, 300, TimeUnit.SECONDS);
        INSTANCE = this;
    }

    public void close() {
        try {
            running = false;
            queue.offer(new URL(POISON_PROTOCOL, NetUtils.LOCALHOST, 0));
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
        try {
            chartFuture.cancel(true);
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }
    
    private void write() throws Exception {
        URL statistics = queue.take();
        if (POISON_PROTOCOL.equals(statistics.getProtocol())) {
            return;
        }
        String timestamp = statistics.getParameter(Constants.TIMESTAMP_KEY);
        Date now;
        if (timestamp == null || timestamp.length() == 0) {
            now = new Date();
        } else if (timestamp.length() == "yyyyMMddHHmmss".length()) {
            now = new SimpleDateFormat("yyyyMMddHHmmss").parse(timestamp);
        } else {
            now = new Date(Long.parseLong(timestamp));
        }
        String day = new SimpleDateFormat("yyyyMMdd").format(now);
        SimpleDateFormat format = new SimpleDateFormat("HHmm");
        for (String key : types) {
            try {
                String type;
                String consumer;
                String provider;
                if (statistics.hasParameter(PROVIDER)) {
                    type = CONSUMER;
                    consumer = statistics.getHost();
                    provider = statistics.getParameter(PROVIDER);
                    int i = provider.indexOf(':');
                    if (i > 0) {
                        provider = provider.substring(0, i);
                    }
                } else {
                    type = PROVIDER;
                    consumer = statistics.getParameter(CONSUMER);
                    int i = consumer.indexOf(':');
                    if (i > 0) {
                        consumer = consumer.substring(0, i);
                    }
                    provider = statistics.getHost();
                }
                String filename = statisticsDirectory 
                        + "/" + day 
                        + "/" + statistics.getServiceInterface() 
                        + "/" + statistics.getParameter(METHOD) 
                        + "/" + consumer 
                        + "/" + provider 
                        + "/" + type + "." + key;
                File file = new File(filename);
                File dir = file.getParentFile();
                if (dir != null && ! dir.exists()) {
                    dir.mkdirs();
                }
                FileWriter writer = new FileWriter(file, true);
                try {
                    writer.write(format.format(now) + " " + statistics.getParameter(key, 0) + "\n");
                    writer.flush();
                } finally {
                    writer.close();
                }
            } catch (Throwable t) {
                logger.error(t.getMessage(), t);
            }
        }
    }

    private void draw() {
        File rootDir = new File(statisticsDirectory);
        if (! rootDir.exists()) {
            return;
        }
        File[] dateDirs = rootDir.listFiles();
        for (File dateDir : dateDirs) {
            File[] serviceDirs = dateDir.listFiles();
            for (File serviceDir : serviceDirs) {
                File[] methodDirs = serviceDir.listFiles();
                for (File methodDir : methodDirs) {
                    String methodUri = chartsDirectory + "/" + dateDir.getName() + "/" + serviceDir.getName() + "/" + methodDir.getName();
                    
                    File successFile = new File(methodUri + "/" + SUCCESS + ".png");
                    long successModified = successFile.lastModified();
                    boolean successChanged = false;
                    Map<String, long[]> successData = new HashMap<String, long[]>();
                    double[] successSummary = new double[4];
                    
                    File elapsedFile = new File(methodUri + "/" + ELAPSED + ".png");
                    long elapsedModified = elapsedFile.lastModified();
                    boolean elapsedChanged = false;
                    Map<String, long[]> elapsedData = new HashMap<String, long[]>();
                    double[] elapsedSummary = new double[4];
                    long elapsedMax = 0;
                    
                    File[] consumerDirs = methodDir.listFiles();
                    for (File consumerDir : consumerDirs) {
                        File[] providerDirs = consumerDir.listFiles();
                        for (File providerDir : providerDirs) {
                            File consumerSuccessFile = new File(providerDir, CONSUMER + "." + SUCCESS);
                            File providerSuccessFile = new File(providerDir, PROVIDER + "." + SUCCESS);
                            appendData(new File[] {consumerSuccessFile, providerSuccessFile}, successData, successSummary);
                            if (consumerSuccessFile.lastModified() > successModified 
                                    || providerSuccessFile.lastModified() > successModified) {
                                successChanged = true;
                            }
                            
                            File consumerElapsedFile = new File(providerDir, CONSUMER + "." + ELAPSED);
                            File providerElapsedFile = new File(providerDir, PROVIDER + "." + ELAPSED);
                            appendData(new File[] {consumerElapsedFile, providerElapsedFile}, elapsedData, elapsedSummary);
                            elapsedMax = Math.max(elapsedMax, CountUtils.max(new File(providerDir, CONSUMER + "." + MAX_ELAPSED)));
                            elapsedMax = Math.max(elapsedMax, CountUtils.max(new File(providerDir, PROVIDER + "." + MAX_ELAPSED)));
                            if (consumerElapsedFile.lastModified() > elapsedModified 
                                    || providerElapsedFile.lastModified() > elapsedModified) {
                                elapsedChanged = true;
                            }
                        }
                    }
                    if (elapsedChanged) {
                        divData(elapsedData, successData);
                        elapsedSummary[0] = elapsedMax;
                        elapsedSummary[1] = -1;
                        elapsedSummary[2] = successSummary[3] == 0 ? 0 : elapsedSummary[3] / successSummary[3];
                        elapsedSummary[3] = -1;
                        createChart("ms/t", serviceDir.getName(), methodDir.getName(), dateDir.getName(), new String[] {CONSUMER, PROVIDER}, elapsedData, elapsedSummary, elapsedFile.getAbsolutePath());
                    }
                    if (successChanged) {
                        divData(successData, 60);
                        successSummary[0] = successSummary[0] / 60;
                        successSummary[1] = successSummary[1] / 60;
                        successSummary[2] = successSummary[2] / 60;
                        createChart("t/s", serviceDir.getName(), methodDir.getName(), dateDir.getName(), new String[] {CONSUMER, PROVIDER}, successData, successSummary, successFile.getAbsolutePath());
                    }
                }
            }
        }
    }
    
    private void divData(Map<String, long[]> successMap, long unit) {
        for (long[] success : successMap.values()) {
            for (int i = 0; i < success.length; i ++) {
                success[i] = success[i] / unit;
            }
        }
    }
    
    private void divData(Map<String, long[]> elapsedMap, Map<String, long[]> successMap) {
        for (Map.Entry<String, long[]> entry : elapsedMap.entrySet()) {
            long[] elapsed = entry.getValue();
            long[] success = successMap.get(entry.getKey());
            for (int i = 0; i < elapsed.length; i ++) {
                elapsed[i] = success[i] == 0 ? 0 : elapsed[i] / success[i];
            }
        }
    }

    private void appendData(File[] files, Map<String, long[]> data, double[] summary) {
        for (int i = 0; i < files.length; i ++) {
            File file = files[i];
            if (! file.exists()) {
                continue;
            }
            try {
                BufferedReader reader = new BufferedReader(new FileReader(file));
                try {
                    int sum = 0;
                    int cnt = 0;
                    String line;
                    while ((line = reader.readLine()) != null) {
                        int index = line.indexOf(" ");
                        if (index > 0) {
                            String key = line.substring(0, index).trim();
                            long value = Long.parseLong(line.substring(index + 1).trim());
                            long[] values = data.get(key);
                            if (values == null) {
                                values = new long[files.length];
                                data.put(key, values);
                            }
                            values[i] += value;
                            summary[0] = Math.max(summary[0], values[i]);
                            summary[1] = summary[1] == 0 ? values[i] : Math.min(summary[1], values[i]);
                            sum += value;
                            cnt ++;
                        }
                    }
                    if (i == 0) {
                        summary[3] += sum;
                        summary[2] = (summary[2] + (sum / cnt)) / 2;
                    }
                } finally {
                    reader.close();
                }
            } catch (IOException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
    
    private static void createChart(String key, String service, String method, String date, String[] types, Map<String, long[]> data, double[] summary, String path) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
        DecimalFormat numberFormat = new DecimalFormat("###,##0.##");
        TimeSeriesCollection xydataset = new TimeSeriesCollection();
        for (int i = 0; i < types.length; i ++) {
            String type = types[i];
            TimeSeries timeseries = new TimeSeries(type);
            for (Map.Entry<String, long[]> entry : data.entrySet()) {
                try {
                    timeseries.add(new Minute(dateFormat.parse(date + entry.getKey())), entry.getValue()[i]);
                } catch (ParseException e) {
                    logger.error(e.getMessage(), e);
                }
            }
            xydataset.addSeries(timeseries);
        }
        JFreeChart jfreechart = ChartFactory.createTimeSeriesChart(
                "max: " + numberFormat.format(summary[0]) + (summary[1] >=0 ? " min: " + numberFormat.format(summary[1]) : "") 
                + " avg: " + numberFormat.format(summary[2]) + (summary[3] >=0 ? " sum: " + numberFormat.format(summary[3]) : ""), 
                toDisplayService(service) + "  " + method + "  " + toDisplayDate(date), key, xydataset, true, true, false);
        jfreechart.setBackgroundPaint(Color.WHITE);
        XYPlot xyplot = (XYPlot) jfreechart.getPlot();
        xyplot.setBackgroundPaint(Color.WHITE);
        xyplot.setDomainGridlinePaint(Color.GRAY);
        xyplot.setRangeGridlinePaint(Color.GRAY);
        xyplot.setDomainGridlinesVisible(true);
        xyplot.setRangeGridlinesVisible(true);
        DateAxis dateaxis = (DateAxis) xyplot.getDomainAxis();
        dateaxis.setDateFormatOverride(new SimpleDateFormat("HH:mm"));
        BufferedImage image = jfreechart.createBufferedImage(600, 300);
        try {
            if (logger.isInfoEnabled()) {
                logger.info("write chart: " + path);
            }
            File methodChartFile = new File(path);
            File methodChartDir = methodChartFile.getParentFile();
            if (methodChartDir != null && ! methodChartDir.exists()) {
                methodChartDir.mkdirs();
            }
            FileOutputStream output = new FileOutputStream(methodChartFile);
            try {
                ImageIO.write(image, "png", output);
                output.flush();
            } finally {
                output.close();
            }
        } catch (IOException e) {
            logger.warn(e.getMessage(), e);
        }
    }
    
    private static String toDisplayService(String service) {
        int i = service.lastIndexOf('.');
        if (i >= 0) {
            return service.substring(i + 1);
        }
        return service;
    }
    
    private static String toDisplayDate(String date) {
        try {
            return new SimpleDateFormat("yyyy-MM-dd").format(new SimpleDateFormat("yyyyMMdd").parse(date));
        } catch (ParseException e) {
            return date;
        }
    }

    public void count(URL statistics) {
        collect(statistics);
    }

    public void collect(URL statistics) {
        queue.offer(statistics);
        if (logger.isInfoEnabled()) {
            logger.info("collect statistics: " + statistics);
        }
    }

	public List<URL> lookup(URL query) {
		// TODO Auto-generated method stub
		return null;
	}

}