/*
 *  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;


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.Controls;
import org.apache.directory.studio.connection.core.StudioControl;
import org.apache.directory.studio.connection.core.io.ConnectionIOException;
import org.apache.directory.studio.ldapbrowser.core.model.BookmarkParameter;
import org.apache.directory.studio.ldapbrowser.core.model.IBookmark;
import org.apache.directory.studio.ldapbrowser.core.model.IBrowserConnection;
import org.apache.directory.studio.ldapbrowser.core.model.ISearch;
import org.apache.directory.studio.ldapbrowser.core.model.SearchParameter;
import org.apache.directory.studio.ldapbrowser.core.model.impl.Bookmark;
import org.apache.directory.studio.ldapbrowser.core.model.impl.Search;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.eclipse.osgi.util.NLS;


/**
 * This class is used to read/write the 'connections.xml' file.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class BrowserConnectionIO
{
    // XML tags
    private static final String BROWSER_CONNECTIONS_TAG = "browserConnections"; //$NON-NLS-1$

    private static final String BROWSER_CONNECTION_TAG = "browserConnection"; //$NON-NLS-1$
    private static final String ID_TAG = "id"; //$NON-NLS-1$

    private static final String SEARCHES_TAG = "searches"; //$NON-NLS-1$
    private static final String SEARCH_PARAMETER_TAG = "searchParameter"; //$NON-NLS-1$
    private static final String NAME_TAG = "name"; //$NON-NLS-1$
    private static final String SEARCH_BASE_TAG = "searchBase"; //$NON-NLS-1$
    private static final String FILTER_TAG = "filer"; //$NON-NLS-1$
    private static final String RETURNING_ATTRIBUTES_TAG = "returningAttributes"; //$NON-NLS-1$
    private static final String RETURNING_ATTRIBUTE_TAG = "returningAttribute"; //$NON-NLS-1$
    private static final String VALUE_TAG = "value"; //$NON-NLS-1$
    private static final String SCOPE_TAG = "scope"; //$NON-NLS-1$
    private static final String TIME_LIMIT_TAG = "timeLimit"; //$NON-NLS-1$
    private static final String COUNT_LIMIT_TAG = "countLimit"; //$NON-NLS-1$
    private static final String ALIASES_DEREFERENCING_METHOD_TAG = "aliasesDereferencingMethod"; //$NON-NLS-1$
    private static final String REFERRALS_HANDLING_METHOD_TAG = "referralsHandlingMethod"; //$NON-NLS-1$
    private static final String PAGED_SEARCH_SCROLL_MODE = "pagedSearchScrollMode"; //$NON-NLS-1$
    private static final String CONTROLS_TAG = "controls"; //$NON-NLS-1$
    private static final String CONTROL_TAG = "control"; //$NON-NLS-1$
    private static final String OID_TAG = "oid"; //$NON-NLS-1$
    private static final String IS_CRITICAL_TAG = "isCritical"; //$NON-NLS-1$

    private static final String BOOKMARKS_TAG = "bookmarks"; //$NON-NLS-1$
    private static final String BOOKMARK_PARAMETER_TAG = "bookmarkParameter"; //$NON-NLS-1$
    private static final String DN_TAG = "dn"; //$NON-NLS-1$

    // Scope values
    private static final String SCOPE_OBJECT = "OBJECT"; //$NON-NLS-1$
    private static final String SCOPE_ONELEVEL = "ONELEVEL"; //$NON-NLS-1$
    private static final String SCOPE_SUBTREE = "SUBTREE"; //$NON-NLS-1$
    private static final String SCOPE_OBJECT_2 = "base"; //$NON-NLS-1$
    private static final String SCOPE_ONELEVEL_2 = "one"; //$NON-NLS-1$
    private static final String SCOPE_SUBTREE_2 = "sub"; //$NON-NLS-1$


    /**
     * Loads the browser connections using the input stream.
     *
     * @param stream
     *      the input stream
     * @param browserConnectionMap
     *      the map of browser connections
     * @throws ConnectionIOException 
     *      if an error occurs when converting the document
     */
    public static void load( InputStream stream, Map<String, IBrowserConnection> browserConnectionMap )
        throws ConnectionIOException
    {
        SAXReader saxReader = new SAXReader();
        Document document = null;

        try
        {
            document = saxReader.read( stream );
        }
        catch ( DocumentException e )
        {
            throw new ConnectionIOException( e.getMessage() );
        }

        Element rootElement = document.getRootElement();
        if ( !rootElement.getName().equals( BROWSER_CONNECTIONS_TAG ) )
        {
            throw new ConnectionIOException( BrowserCoreMessages.BrowserConnectionIO_TheFileDoesNotSeemToBeValid );
        }

        for ( Iterator<?> i = rootElement.elementIterator( BROWSER_CONNECTION_TAG ); i.hasNext(); )
        {
            Element browserConnectionElement = ( Element ) i.next();
            readBrowserConnection( browserConnectionElement, browserConnectionMap );
        }
    }


    /**
     * Reads a browser connection from the given Element.
     *
     * @param element
     *      the element
     * @param browserConnectionMap
     *      the map of browser connections     *      
     * @throws ConnectionIOException
     *      if an error occurs when converting values
     */
    private static void readBrowserConnection( Element element, Map<String, IBrowserConnection> browserConnectionMap )
        throws ConnectionIOException
    {
        // ID
        Attribute idAttribute = element.attribute( ID_TAG );
        if ( idAttribute != null )
        {
            String id = idAttribute.getValue();
            IBrowserConnection browserConnection = browserConnectionMap.get( id );

            if ( browserConnection != null )
            {
                Element searchesElement = element.element( SEARCHES_TAG );
                if ( searchesElement != null )
                {
                    for ( Iterator<?> i = searchesElement.elementIterator( SEARCH_PARAMETER_TAG ); i.hasNext(); )
                    {
                        Element searchParameterElement = ( Element ) i.next();
                        SearchParameter searchParameter = readSearch( searchParameterElement, browserConnection );
                        ISearch search = new Search( browserConnection, searchParameter );
                        browserConnection.getSearchManager().addSearch( search );
                    }
                }

                Element bookmarksElement = element.element( BOOKMARKS_TAG );
                if ( bookmarksElement != null )
                {
                    for ( Iterator<?> i = bookmarksElement.elementIterator( BOOKMARK_PARAMETER_TAG ); i.hasNext(); )
                    {
                        Element bookmarkParameterElement = ( Element ) i.next();
                        BookmarkParameter bookmarkParameter = readBookmark( bookmarkParameterElement, browserConnection );
                        IBookmark bookmark = new Bookmark( browserConnection, bookmarkParameter );
                        browserConnection.getBookmarkManager().addBookmark( bookmark );
                    }
                }
            }
        }
    }


    private static SearchParameter readSearch( Element searchParameterElement, IBrowserConnection browserConnection )
        throws ConnectionIOException
    {
        SearchParameter searchParameter = new SearchParameter();

        // Name
        Attribute nameAttribute = searchParameterElement.attribute( NAME_TAG );
        if ( nameAttribute != null )
        {
            searchParameter.setName( nameAttribute.getValue() );
        }

        // Search base
        Attribute searchBaseAttribute = searchParameterElement.attribute( SEARCH_BASE_TAG );
        if ( searchBaseAttribute != null )
        {
            try
            {
                searchParameter.setSearchBase( new Dn( searchBaseAttribute.getValue() ) );
            }
            catch ( LdapInvalidDnException e )
            {
                throw new ConnectionIOException( NLS.bind(
                    BrowserCoreMessages.BrowserConnectionIO_UnableToParseSearchBase,
                    new String[]
                        { searchParameter.getName(), searchBaseAttribute.getValue() } ) );
            }
        }

        // Filter
        Attribute filterAttribute = searchParameterElement.attribute( FILTER_TAG );
        if ( filterAttribute != null )
        {
            searchParameter.setFilter( filterAttribute.getValue() );
        }

        // Returning Attributes
        Element returningAttributesElement = searchParameterElement.element( RETURNING_ATTRIBUTES_TAG );
        if ( returningAttributesElement != null )
        {
            List<String> returningAttributes = new ArrayList<String>();
            for ( Iterator<?> i = returningAttributesElement.elementIterator( RETURNING_ATTRIBUTE_TAG ); i.hasNext(); )
            {
                Element returningAttributeElement = ( Element ) i.next();

                Attribute valueAttribute = returningAttributeElement.attribute( VALUE_TAG );
                if ( valueAttribute != null )
                {
                    returningAttributes.add( valueAttribute.getValue() );
                }
            }
            searchParameter.setReturningAttributes( returningAttributes
                .toArray( new String[returningAttributes.size()] ) );
        }

        // Scope
        Attribute scopeAttribute = searchParameterElement.attribute( SCOPE_TAG );
        if ( scopeAttribute != null )
        {
            try
            {
                searchParameter.setScope( convertSearchScope( scopeAttribute.getValue() ) );
            }
            catch ( IllegalArgumentException e )
            {
                throw new ConnectionIOException( NLS.bind(
                    BrowserCoreMessages.BrowserConnectionIO_UnableToParseScope, new String[]
                        { searchParameter.getName(), scopeAttribute.getValue() } ) );
            }
        }

        // Time limit
        Attribute timeLimitAttribute = searchParameterElement.attribute( TIME_LIMIT_TAG );
        if ( timeLimitAttribute != null )
        {
            try
            {
                searchParameter.setTimeLimit( Integer.parseInt( timeLimitAttribute.getValue() ) );
            }
            catch ( NumberFormatException e )
            {
                throw new ConnectionIOException( NLS.bind(
                    BrowserCoreMessages.BrowserConnectionIO_UnableToParseTimeLimit,
                    new String[]
                        { searchParameter.getName(), timeLimitAttribute.getValue() } ) );
            }
        }

        // Count limit
        Attribute countLimitAttribute = searchParameterElement.attribute( COUNT_LIMIT_TAG );
        if ( countLimitAttribute != null )
        {
            try
            {
                searchParameter.setCountLimit( Integer.parseInt( countLimitAttribute.getValue() ) );
            }
            catch ( NumberFormatException e )
            {
                throw new ConnectionIOException( NLS.bind(
                    BrowserCoreMessages.BrowserConnectionIO_UnableToParseCountLimit,
                    new String[]
                        { searchParameter.getName(), countLimitAttribute.getValue() } ) );
            }
        }

        // Alias dereferencing method
        Attribute aliasesDereferencingMethodAttribute = searchParameterElement
            .attribute( ALIASES_DEREFERENCING_METHOD_TAG );
        if ( aliasesDereferencingMethodAttribute != null )
        {
            try
            {
                searchParameter.setAliasesDereferencingMethod( Connection.AliasDereferencingMethod
                    .valueOf( aliasesDereferencingMethodAttribute.getValue() ) );
            }
            catch ( IllegalArgumentException e )
            {
                throw new ConnectionIOException(
                    NLS.bind(
                        BrowserCoreMessages.BrowserConnectionIO_UnableToParseAliasesDereferencingMethod,
                        new String[]
                            { searchParameter.getName(), aliasesDereferencingMethodAttribute.getValue() } ) );
            }
        }

        // Referrals handling method
        Attribute referralsHandlingMethodAttribute = searchParameterElement.attribute( REFERRALS_HANDLING_METHOD_TAG );
        if ( referralsHandlingMethodAttribute != null )
        {
            try
            {
                searchParameter.setReferralsHandlingMethod( Connection.ReferralHandlingMethod
                    .valueOf( referralsHandlingMethodAttribute.getValue() ) );
            }
            catch ( IllegalArgumentException e )
            {
                throw new ConnectionIOException(
                    NLS.bind(
                        BrowserCoreMessages.BrowserConnectionIO_UnableToParseReferralsHandlingMethod,
                        new String[]
                            { searchParameter.getName(), referralsHandlingMethodAttribute.getValue() } ) );
            }
        }

        // Paged search scroll mode
        Attribute pagedSearchScrollModeAttribute = searchParameterElement.attribute( PAGED_SEARCH_SCROLL_MODE );
        if ( pagedSearchScrollModeAttribute != null )
        {
            searchParameter.setPagedSearchScrollMode( Boolean.valueOf( pagedSearchScrollModeAttribute.getValue() ) );
        }

        // Controls
        Element controlsElement = searchParameterElement.element( CONTROLS_TAG );
        if ( controlsElement != null )
        {
            for ( Iterator<?> i = controlsElement.elementIterator( CONTROL_TAG ); i.hasNext(); )
            {
                Element controlElement = ( Element ) i.next();
                Attribute oidAttribute = controlElement.attribute( OID_TAG );
                Attribute isCriticalAttribute = controlElement.attribute( IS_CRITICAL_TAG );
                Attribute valueAttribute = controlElement.attribute( VALUE_TAG );

                try
                {
                    if ( oidAttribute != null && isCriticalAttribute != null && valueAttribute != null )
                    {
                        byte[] bytes = Base64.getDecoder().decode( valueAttribute.getValue() );
                        Control control = Controls.create( oidAttribute.getValue(),
                            Boolean.valueOf( isCriticalAttribute.getValue() ), bytes );
                        searchParameter.getControls().add( control );
                    }
                    else if ( valueAttribute != null )
                    {
                        // Backward compatibility: read objects using Java serialization
                        byte[] bytes = Base64.getDecoder().decode( valueAttribute.getValue() );
                        ByteArrayInputStream bais = null;
                        ObjectInputStream ois = null;
                        bais = new ByteArrayInputStream( bytes );
                        ois = new ObjectInputStream( bais );
                        StudioControl studioControl = ( StudioControl ) ois.readObject();
                        Control control = Controls.create( studioControl.getOid(),
                            studioControl.isCritical(), studioControl.getControlValue() );
                        searchParameter.getControls().add( control );
                        ois.close();
                    }
                }
                catch ( Exception e )
                {
                    throw new ConnectionIOException( NLS.bind(
                        BrowserCoreMessages.BrowserConnectionIO_UnableToParseControl, new String[]
                        { searchParameter.getName(), valueAttribute.getValue() } ) );
                }
            }
        }

        return searchParameter;
    }


    private static BookmarkParameter readBookmark( Element bookmarkParameterElement,
        IBrowserConnection browserConnection ) throws ConnectionIOException
    {
        BookmarkParameter bookmarkParameter = new BookmarkParameter();

        // Name
        Attribute nameAttribute = bookmarkParameterElement.attribute( NAME_TAG );
        if ( nameAttribute != null )
        {
            bookmarkParameter.setName( nameAttribute.getValue() );
        }

        // Dn
        Attribute dnAttribute = bookmarkParameterElement.attribute( DN_TAG );
        if ( dnAttribute != null )
        {
            try
            {
                bookmarkParameter.setDn( new Dn( dnAttribute.getValue() ) );
            }
            catch ( LdapInvalidDnException e )
            {
                throw new ConnectionIOException( NLS.bind( BrowserCoreMessages.BrowserConnectionIO_UnableToParseDn,
                    new String[]
                        { bookmarkParameter.getName(), dnAttribute.getValue() } ) );
            }
        }

        return bookmarkParameter;
    }


    /**
     * Saves the browser connections using the output stream.
     *
     * @param stream
     *      the OutputStream
     * @param browserConnectionMap
     *      the map of browser connections
     * @throws IOException
     *      if an I/O error occurs
     */
    public static void save( OutputStream stream, Map<String, IBrowserConnection> browserConnectionMap )
        throws IOException
    {
        // Creating the Document
        Document document = DocumentHelper.createDocument();

        // Creating the root element
        Element root = document.addElement( BROWSER_CONNECTIONS_TAG );

        if ( browserConnectionMap != null )
        {
            for ( IBrowserConnection browserConnection : browserConnectionMap.values() )
            {
                writeBrowserConnection( root, browserConnection );
            }
        }

        // Writing the file to disk
        OutputFormat outformat = OutputFormat.createPrettyPrint();
        outformat.setEncoding( "UTF-8" ); //$NON-NLS-1$
        XMLWriter writer = new XMLWriter( stream, outformat );
        writer.write( document );
        writer.flush();
    }


    /**
     * Writes the given browser connection to the given parent Element.
     *
     * @param parent
     *      the parent Element
     * @param browserConnection
     *      the browser connection
     * @throws IOException 
     */
    private static void writeBrowserConnection( Element parent, IBrowserConnection browserConnection )
        throws IOException
    {
        Element browserConnectionElement = parent.addElement( BROWSER_CONNECTION_TAG );

        // ID
        browserConnectionElement.addAttribute( ID_TAG, browserConnection.getConnection().getId() );

        // Searches
        Element searchesElement = browserConnectionElement.addElement( SEARCHES_TAG );
        List<ISearch> searches = browserConnection.getSearchManager().getSearches();
        for ( ISearch search : searches )
        {
            Element searchParameterElement = searchesElement.addElement( SEARCH_PARAMETER_TAG );
            writeSearch( searchParameterElement, search.getSearchParameter() );
        }

        // Bookmarks
        Element bookmarksElement = browserConnectionElement.addElement( BOOKMARKS_TAG );
        IBookmark[] bookmarks = browserConnection.getBookmarkManager().getBookmarks();
        for ( IBookmark bookmark : bookmarks )
        {
            Element bookmarkParameterElement = bookmarksElement.addElement( BOOKMARK_PARAMETER_TAG );
            writeBookmark( bookmarkParameterElement, bookmark.getBookmarkParameter() );
        }
    }


    private static void writeSearch( Element searchParameterElement, SearchParameter searchParameter )
        throws IOException
    {
        // Name
        searchParameterElement.addAttribute( NAME_TAG, searchParameter.getName() );

        // Search base
        String searchBase = searchParameter.getSearchBase() != null ? searchParameter.getSearchBase().getName() : ""; //$NON-NLS-1$
        searchParameterElement.addAttribute( SEARCH_BASE_TAG, searchBase );

        // Filter
        searchParameterElement.addAttribute( FILTER_TAG, searchParameter.getFilter() );

        // Returning Attributes
        Element returningAttributesElement = searchParameterElement.addElement( RETURNING_ATTRIBUTES_TAG );
        for ( String ra : searchParameter.getReturningAttributes() )
        {
            Element raElement = returningAttributesElement.addElement( RETURNING_ATTRIBUTE_TAG );
            raElement.addAttribute( VALUE_TAG, ra );
        }

        // Scope
        searchParameterElement.addAttribute( SCOPE_TAG, convertSearchScope( searchParameter.getScope() ) );

        // Time limit
        searchParameterElement.addAttribute( TIME_LIMIT_TAG, "" + searchParameter.getTimeLimit() ); //$NON-NLS-1$

        // Count limit
        searchParameterElement.addAttribute( COUNT_LIMIT_TAG, "" + searchParameter.getCountLimit() ); //$NON-NLS-1$

        // Alias dereferencing method
        searchParameterElement.addAttribute( ALIASES_DEREFERENCING_METHOD_TAG, searchParameter
            .getAliasesDereferencingMethod().toString() );

        // Referrals handling method
        searchParameterElement.addAttribute( REFERRALS_HANDLING_METHOD_TAG, searchParameter
            .getReferralsHandlingMethod().toString() );

        // Paged search scroll mode
        searchParameterElement.addAttribute( PAGED_SEARCH_SCROLL_MODE, "" + searchParameter.isPagedSearchScrollMode() );

        // Controls
        Element controlsElement = searchParameterElement.addElement( CONTROLS_TAG );
        for ( Control control : searchParameter.getControls() )
        {
            byte[] bytes = Controls.getEncodedValue( control );
            String controlsValue = new String( Base64.getEncoder().encode( bytes ), StandardCharsets.UTF_8 );

            Element controlElement = controlsElement.addElement( CONTROL_TAG );
            controlElement.addAttribute( OID_TAG, control.getOid() );
            controlElement.addAttribute( IS_CRITICAL_TAG, "" + control.isCritical() );
            controlElement.addAttribute( VALUE_TAG, controlsValue );
        }
    }


    private static void writeBookmark( Element bookmarkParameterElement, BookmarkParameter bookmarkParameter )
    {
        // Name
        bookmarkParameterElement.addAttribute( NAME_TAG, bookmarkParameter.getName() );

        // Dn
        String dn = bookmarkParameter.getDn() != null ? bookmarkParameter.getDn().getName() : ""; //$NON-NLS-1$
        bookmarkParameterElement.addAttribute( DN_TAG, dn );
    }


    /**
     * Converts the given search scope to a string.
     *
     * @param scope the search scope
     * @return the converted string for the search scope
     * @see  https://issues.apache.org/jira/browse/DIRSTUDIO-771
     */
    private static String convertSearchScope( SearchScope scope )
    {
        if ( scope != null )
        {
            switch ( scope )
            {
                case OBJECT:
                    return SCOPE_OBJECT;
                case ONELEVEL:
                    return SCOPE_ONELEVEL;
                case SUBTREE:
                    return SCOPE_SUBTREE;
            }
        }

        return SCOPE_SUBTREE;
    }


    /**
     * Converts the given string to a search scope.
     * 
     * @param scope the scope string
     * @return the corresponding search scope
     * @throws IllegalArgumentException if the string could not be converted
     * @see  https://issues.apache.org/jira/browse/DIRSTUDIO-771
     */
    private static SearchScope convertSearchScope( String scope ) throws IllegalArgumentException
    {
        if ( ( SCOPE_OBJECT.equalsIgnoreCase( scope ) || SCOPE_OBJECT_2.equalsIgnoreCase( scope ) ) )
        {
            return SearchScope.OBJECT;
        }
        else if ( ( SCOPE_ONELEVEL.equalsIgnoreCase( scope ) || SCOPE_ONELEVEL_2.equalsIgnoreCase( scope ) ) )
        {
            return SearchScope.ONELEVEL;
        }
        else if ( ( SCOPE_SUBTREE.equalsIgnoreCase( scope ) || SCOPE_SUBTREE_2.equalsIgnoreCase( scope ) ) )
        {
            return SearchScope.SUBTREE;
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }
}
