package org.codelibs.elasticsearch.df.content.xls;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.codelibs.elasticsearch.df.content.ContentType;
import org.codelibs.elasticsearch.df.content.DataContent;
import org.codelibs.elasticsearch.df.util.MapUtils;
import org.codelibs.elasticsearch.df.util.RequestUtil;
import org.codelibs.elasticsearch.df.util.StringUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;

public class XlsContent extends DataContent {
    private static final Logger logger = LogManager.getLogger(XlsContent.class);

    private static final int SXSSF_FLUSH_COUNT = 1000;

    private static final String DEFAULT_HEADER_COLUMN = "-";

    private final boolean appendHeader;

    private boolean headerFlushed = false;

    private Set<String> headerSet;

    private boolean modifiableFieldSet;

    private final boolean isExcel2007;

    public XlsContent(final Client client, final RestRequest request, final ContentType contentType, final boolean isExcel2007) {
        super(client, request, contentType);

        appendHeader = request.paramAsBoolean("append.header", true);
        String fieldsName = "fields_name";
        if (request.hasParam("fl")) {
            fieldsName = "fl";
        }
        final String[] fields = request.paramAsStringArray(fieldsName,
                StringUtils.EMPTY_STRINGS);
        if (fields.length == 0) {
            headerSet = new LinkedHashSet<>();
            modifiableFieldSet = true;
        } else {
            final Set<String> fieldSet = new LinkedHashSet<>();
            for (final String field : fields) {
                fieldSet.add(field.trim());
            }
            headerSet = Collections.unmodifiableSet(fieldSet);
            modifiableFieldSet = false;
        }

        this.isExcel2007 = isExcel2007;

        if (logger.isDebugEnabled()) {
            logger.debug("appendHeader: {}, headerSet: {}, isExcel2007: {}",
                    appendHeader, headerSet, isExcel2007);
        }
    }

    @Override
    public void write(final File outputFile, final SearchResponse response, final RestChannel channel,
                      final ActionListener<Void> listener) {
        try {
            final OnLoadListener onLoadListener = new OnLoadListener(outputFile, listener);
            onLoadListener.onResponse(response);
        } catch (final Exception e) {
            listener.onFailure(new ElasticsearchException("Failed to write data.", e));
        }
    }

    protected class OnLoadListener implements ActionListener<SearchResponse> {

        protected ActionListener<Void> listener;

        protected File outputFile;

        private Workbook workbook;

        private Sheet sheet;

        private int currentRowNumber = 0;

        protected OnLoadListener(final File outputFile, final ActionListener<Void> listener) {
            this.outputFile = outputFile;
            this.listener = listener;

            if (isExcel2007) {
                final SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    sm.checkPermission(new SpecialPermission());
                }
                workbook = AccessController
                        .doPrivileged((PrivilegedAction<Workbook>) () -> new SXSSFWorkbook(-1));
                sheet = AccessController
                        .doPrivileged((PrivilegedAction<Sheet>) () -> workbook.createSheet());
            } else {
                workbook = new HSSFWorkbook();
                sheet = workbook.createSheet();
            }
        }

        private void flushSheet(final int currentCount, final Sheet sheet)
                throws IOException {
            if (sheet instanceof SXSSFSheet) {
                if (currentCount % SXSSF_FLUSH_COUNT == 0) {
                    ((SXSSFSheet) sheet).flushRows(0);
                }
            }
        }

        private void disposeWorkbook(final Workbook workbook) {
            if (workbook instanceof SXSSFWorkbook) {
                ((SXSSFWorkbook) workbook).dispose();
            }
        }

        @Override
        public void onResponse(final SearchResponse response) {
            final String scrollId = response.getScrollId();

            try {
                final SearchHits hits = response.getHits();

                final int size = hits.getHits().length;
                if (logger.isDebugEnabled()) {
                    logger.debug("scrollId: {}, totalHits: {}, hits: {}, current: {}",
                            scrollId, hits.getTotalHits(), size, (currentRowNumber + size));
                }
                for (final SearchHit hit : hits) {
                    final Map<String, Object> sourceMap = hit.getSourceAsMap();
                    final Map<String, Object> dataMap = new HashMap<>();
                    MapUtils.convertToFlatMap("", sourceMap, dataMap);

                    for (final String key : dataMap.keySet()) {
                        if (modifiableFieldSet && !headerSet.contains(key)) {
                            headerSet.add(key);
                        }
                    }

                    if (!headerFlushed && appendHeader) {
                        final Row headerRow = sheet.createRow(0);
                        int count = 0;
                        for (final String value : headerSet) {
                            final Cell cell = headerRow.createCell(count);
                            cell.setCellValue(value);
                            count++;
                        }
                        headerFlushed = true;
                    }

                    final Row row = sheet.createRow(appendHeader ? currentRowNumber + 1
                                    : currentRowNumber);

                    int count = 0;
                    for (final String name : headerSet) {
                        final Object value = dataMap.get(name);
                        final Cell cell = row.createCell(count);
                        if (value != null
                                && value.toString().trim().length() > 0) {
                            cell.setCellValue(value.toString());
                        } else {
                            cell.setCellValue(DEFAULT_HEADER_COLUMN);
                        }
                        count++;
                    }

                    flushSheet(currentRowNumber, sheet);
                    currentRowNumber++;
                }

                if (size == 0 || scrollId == null) {
                    flushSheet(0, sheet);
                    try (OutputStream stream = new BufferedOutputStream(new FileOutputStream(outputFile))) {
                        final SecurityManager sm = System.getSecurityManager();
                        if (sm != null) {
                            sm.checkPermission(new SpecialPermission());
                        }
                        AccessController
                                .doPrivileged((PrivilegedAction<Void>) () -> {
                                    try {
                                        workbook.write(stream);
                                    } catch (final IOException e) {
                                        throw new ElasticsearchException(e);
                                    }
                                    return null;
                                });

                        stream.flush();
                    } finally {
                        disposeWorkbook(workbook);
                    }
                    // end
                    listener.onResponse(null);
                } else {
                    client.prepareSearchScroll(scrollId)
                            .setScroll(RequestUtil.getScroll(request))
                            .execute(this);
                }
            } catch (final Exception e) {
                onFailure(e);
            }
        }

        @Override
        public void onFailure(final Exception e) {
            listener.onFailure(new ElasticsearchException("Failed to write data.",
                    e));
        }

    }
}