/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.directory.studio.ldapbrowser.core.jobs;


import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.text.translate.CharSequenceTranslator;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.studio.common.core.jobs.StudioProgressMonitor;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.jobs.StudioConnectionRunnableWithProgress;
import org.apache.directory.studio.ldapbrowser.core.BrowserCoreConstants;
import org.apache.directory.studio.ldapbrowser.core.BrowserCoreMessages;
import org.apache.directory.studio.ldapbrowser.core.BrowserCorePlugin;
import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
import org.apache.directory.studio.ldapbrowser.core.model.SearchParameter;
import org.apache.directory.studio.ldapbrowser.core.utils.JNDIUtils;
import org.apache.directory.studio.ldapbrowser.core.utils.Utils;
import org.apache.directory.studio.ldifparser.model.LdifEnumeration;
import org.apache.directory.studio.ldifparser.model.container.LdifContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifContentRecord;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.eclipse.core.runtime.Preferences;


/**
 * Runnable to export directory content to an XLS file.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class ExportXlsRunnable implements StudioConnectionRunnableWithProgress
{
    /** The maximum count limit */
    public static final int MAX_COUNT_LIMIT = 65000;

    /** The postal address decoder. */
    private static CharSequenceTranslator DECODER = Utils.createPostalAddressDecoder( "\n" ); //$NON-NLS-1$;

    /** The filename of the XLS file. */
    private String exportXlsFilename;

    /** The browser connection. */
    private IBrowserConnection browserConnection;

    /** The search parameter. */
    private SearchParameter searchParameter;

    /** The export dn flag. */
    private boolean exportDn;


    /**
     * Creates a new instance of ExportXlsRunnable.
     * 
     * @param exportLdifFilename the export ldif filename
     * @param browserConnection the browser connection
     * @param searchParameter the search parameter
     * @param exportDn true to export the Dn
     */
    public ExportXlsRunnable( String exportLdifFilename, IBrowserConnection browserConnection,
        SearchParameter searchParameter, boolean exportDn )
    {
        this.exportXlsFilename = exportLdifFilename;
        this.browserConnection = browserConnection;
        this.searchParameter = searchParameter;
        this.exportDn = exportDn;
    }


    /**
     * {@inheritDoc}
     */
    public Connection[] getConnections()
    {
        return new Connection[]
            { browserConnection.getConnection() };
    }


    /**
     * {@inheritDoc}
     */
    public String getName()
    {
        return BrowserCoreMessages.jobs__export_xls_name;
    }


    /**
     * {@inheritDoc}
     */
    public Object[] getLockedObjects()
    {
        return new Object[]
            { browserConnection.getUrl() + "_" + DigestUtils.shaHex( exportXlsFilename ) }; //$NON-NLS-1$
    }


    /**
     * {@inheritDoc}
     */
    public String getErrorMessage()
    {
        return BrowserCoreMessages.jobs__export_xls_error;
    }


    /**
     * {@inheritDoc}
     */
    public void run( StudioProgressMonitor monitor )
    {
        monitor.beginTask( BrowserCoreMessages.jobs__export_xls_task, 2 );
        monitor.reportProgress( " " ); //$NON-NLS-1$
        monitor.worked( 1 );

        Preferences coreStore = BrowserCorePlugin.getDefault().getPluginPreferences();
        String valueDelimiter = coreStore.getString( BrowserCoreConstants.PREFERENCE_FORMAT_XLS_VALUEDELIMITER );
        int binaryEncoding = coreStore.getInt( BrowserCoreConstants.PREFERENCE_FORMAT_XLS_BINARYENCODING );

        HSSFWorkbook wb = new HSSFWorkbook();
        HSSFSheet sheet = wb.createSheet( "Export" ); //$NON-NLS-1$

        // header
        HSSFRow headerRow = sheet.createRow( 0 );
        LinkedHashMap<String, Integer> attributeNameMap = new LinkedHashMap<String, Integer>();
        if ( this.exportDn )
        {
            int cellNum = 0;
            attributeNameMap.put( "dn", cellNum ); //$NON-NLS-1$
            createStringCell( headerRow, cellNum ).setCellValue( "dn" ); //$NON-NLS-1$
        }

        // max export
        if ( searchParameter.getCountLimit() < 1 || searchParameter.getCountLimit() > MAX_COUNT_LIMIT )
        {
            searchParameter.setCountLimit( MAX_COUNT_LIMIT );
        }

        // export
        try
        {
            int count = 0;
            exportToXls( browserConnection, searchParameter, sheet, headerRow, count, monitor, attributeNameMap,
                valueDelimiter, binaryEncoding, this.exportDn );
        }
        catch ( Exception e )
        {
            monitor.reportError( e );
        }

        // column width
        for ( int i = 0; i <= sheet.getLastRowNum(); i++ )
        {
            HSSFRow row = sheet.getRow( i );
            for ( short j = 0; row != null && j <= row.getLastCellNum(); j++ )
            {
                HSSFCell cell = row.getCell( j );
                if ( cell != null && cell.getCellType() == HSSFCell.CELL_TYPE_STRING )
                {
                    String value = cell.getStringCellValue();

                    if ( ( short ) ( value.length() * 256 * 1.1 ) > sheet.getColumnWidth( j ) )
                    {
                        sheet.setColumnWidth( j, ( short ) ( value.length() * 256 * 1.1 ) );
                    }
                }
            }
        }

        try
        {
            FileOutputStream fileOut = new FileOutputStream( exportXlsFilename );
            wb.write( fileOut );
            fileOut.close();
        }
        catch ( Exception e )
        {
            monitor.reportError( e );
        }
    }


    /**
     * Exports to XLS.
     * 
     * @param browserConnection the browser connection
     * @param searchParameter the search parameter
     * @param sheet the sheet
     * @param headerRow the header row
     * @param count the count
     * @param monitor the monitor
     * @param attributeNameMap the attribute name map
     * @param valueDelimiter the value delimiter
     * @param binaryEncoding the binary encoding
     * @param exportDn the export dn
     * 
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static void exportToXls( IBrowserConnection browserConnection, SearchParameter searchParameter,
        HSSFSheet sheet, HSSFRow headerRow, int count, StudioProgressMonitor monitor,
        LinkedHashMap<String, Integer> attributeNameMap, String valueDelimiter, int binaryEncoding, boolean exportDn )
        throws IOException
    {
        try
        {
            LdifEnumeration enumeration = ExportLdifRunnable.search( browserConnection, searchParameter, monitor );
            while ( !monitor.isCanceled() && !monitor.errorsReported() && enumeration.hasNext() )
            {
                LdifContainer container = enumeration.next();

                if ( container instanceof LdifContentRecord )
                {
                    LdifContentRecord record = ( LdifContentRecord ) container;
                    recordToHSSFRow( browserConnection, record, sheet, headerRow, attributeNameMap, valueDelimiter,
                        binaryEncoding, exportDn );

                    count++;
                    monitor.reportProgress( BrowserCoreMessages.bind( BrowserCoreMessages.jobs__export_progress,
                        new String[]
                            { Integer.toString( count ) } ) );
                }
            }

        }
        catch ( LdapException ne )
        {
            int ldapStatusCode = JNDIUtils.getLdapStatusCode( ne );
            if ( ldapStatusCode == 3 || ldapStatusCode == 4 || ldapStatusCode == 11 )
            {
                // nothing
            }
            else
            {
                monitor.reportError( ne );
            }
        }
    }


    /**
     * Transforms an LDIF record to an HSSF row.
     * 
     * @param browserConnection the browser connection
     * @param record the record
     * @param sheet the sheet
     * @param headerRow the header row
     * @param headerRowAttributeNameMap the header row attribute name map
     * @param valueDelimiter the value delimiter
     * @param binaryEncoding the binary encoding
     * @param exportDn the export dn
     */
    private static void recordToHSSFRow( IBrowserConnection browserConnection, LdifContentRecord record,
        HSSFSheet sheet, HSSFRow headerRow, Map<String, Integer> headerRowAttributeNameMap, String valueDelimiter,
        int binaryEncoding, boolean exportDn )
    {
        // group multi-valued attributes
        Map<String, String> attributeMap = ExportCsvRunnable.getAttributeMap( null, record, valueDelimiter, "UTF-16", //$NON-NLS-1$
            binaryEncoding );

        CellStyle wrapStyle = sheet.getWorkbook().createCellStyle();
        wrapStyle.setWrapText( true );

        // output attributes
        HSSFRow row = sheet.createRow( sheet.getLastRowNum() + 1 );
        if ( exportDn )
        {
            HSSFCell cell = createStringCell(row, 0 );
            cell.setCellValue( record.getDnLine().getValueAsString() );
        }
        for ( String attributeName : attributeMap.keySet() )
        {
            String value = attributeMap.get( attributeName );

            if ( !headerRowAttributeNameMap.containsKey( attributeName ) )
            {
                int cellNum = headerRowAttributeNameMap.size();
                headerRowAttributeNameMap.put( attributeName, new Integer( cellNum ) );
                HSSFCell cell = createStringCell( headerRow, cellNum );
                cell.setCellValue( attributeName );
            }

            if ( headerRowAttributeNameMap.containsKey( attributeName ) )
            {
                int cellNum = headerRowAttributeNameMap.get( attributeName ).shortValue();
                HSSFCell cell = createStringCell( row, cellNum );
                AttributeType type = browserConnection.getSchema().getAttributeTypeDescription( attributeName );
                if ( SchemaConstants.POSTAL_ADDRESS_SYNTAX.equals( type.getSyntaxOid() ) )
                {
                    // https://poi.apache.org/components/spreadsheet/quick-guide.html#NewLinesInCells
                    value = DECODER.translate( value );
                    cell.setCellStyle( wrapStyle );
                }
                cell.setCellValue( value );
            }
        }

    }


    private static HSSFCell createStringCell( HSSFRow row, int cellNum )
    {
        HSSFCell cell = row.createCell( cellNum );
        cell.setCellType( Cell.CELL_TYPE_STRING );
        return cell;
    }

}
