/*
 *  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 com.example.dao;


import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import javax.naming.InvalidNameException;
import javax.naming.NamingException;

import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.exception.LdapException;
import org.apache.directory.ldap.client.api.message.ModifyRequest;
import org.apache.directory.ldap.client.api.message.SearchResponse;
import org.apache.directory.ldap.client.api.message.SearchResultEntry;
import org.apache.directory.shared.ldap.cursor.Cursor;
import org.apache.directory.shared.ldap.entry.Entry;
import org.apache.directory.shared.ldap.entry.EntryAttribute;
import org.apache.directory.shared.ldap.entry.ModificationOperation;
import org.apache.directory.shared.ldap.entry.Value;
import org.apache.directory.shared.ldap.entry.client.ClientModification;
import org.apache.directory.shared.ldap.entry.client.DefaultClientAttribute;
import org.apache.directory.shared.ldap.entry.client.DefaultClientEntry;
import org.apache.directory.shared.ldap.filter.SearchScope;
import org.apache.directory.shared.ldap.name.DN;
import org.apache.directory.shared.ldap.util.GeneralizedTime;


public abstract class GenericLdapDao<T>
{

    protected String server;
    protected int port;
    protected String name;
    protected String credentials;
    protected String rdnAttribute;
    protected String parentDn;
    protected String[] attributes;
    protected String[] objectClasses;


    public T getById( String id )
    {
        LdapConnection connection = null;
        try
        {
            connection = getLdapConnection();
            DN dn = new DN( rdnAttribute, id, parentDn );
            Entry entry = lookupEntry( connection, dn.getName() );
            if ( entry != null )
            {
                T object = mapFromEntry( entry );
                return object;
            }
            else
            {
                return null;
            }
        }
        catch ( Exception e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            returnLdapConnection( connection );
        }
    }


    public Collection<T> getAll()
    {
        Collection<T> all = new ArrayList<T>();

        LdapConnection connection = null;
        try
        {
            connection = getLdapConnection();

            String filter = "(&";
            for ( String objectClass : objectClasses )
            {
                filter += "(objectClass=" + objectClass + ")";
            }
            filter += ")";

            Cursor<SearchResponse> cursor = connection.search( parentDn, filter, SearchScope.ONELEVEL, attributes );
            while ( cursor.next() )
            {
                SearchResponse response = cursor.get();
                if ( response instanceof SearchResultEntry )
                {
                    SearchResultEntry sre = ( SearchResultEntry ) response;
                    Entry entry = sre.getEntry();
                    T object = mapFromEntry( entry );
                    all.add( object );
                }
                // TODO: handle other response types
            }
        }
        catch ( Exception e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            returnLdapConnection( connection );
        }

        return all;
    }


    public void create( T object )
    {
        LdapConnection connection = null;
        try
        {
            connection = getLdapConnection();
            Entry entry = getEntry( object );
            connection.add( entry );
        }
        catch ( Exception e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            returnLdapConnection( connection );
        }
    }


    public void update( T object )
    {
        LdapConnection connection = null;
        try
        {
            connection = getLdapConnection();

            // get old and new entry
            Entry newEntry = getEntry( object );
            Entry oldEntry = lookupEntry( connection, newEntry.getDn().getName() );

            // generate diff
            ModifyRequest modRequest = new ModifyRequest( newEntry.getDn() );
            for ( EntryAttribute newAttribute : newEntry )
            {
                if ( oldEntry.contains( newAttribute ) )
                {
                    EntryAttribute oldAttribute = oldEntry.get( newAttribute.getId() );
                    if ( !oldAttribute.equals( newAttribute ) )
                    {
                        modRequest.addModification( newAttribute, ModificationOperation.REPLACE_ATTRIBUTE );
                    }
                }
                else
                {
                    modRequest.addModification( newAttribute, ModificationOperation.REPLACE_ATTRIBUTE );
                }
            }
            for ( EntryAttribute oldAttribute : oldEntry )
            {
                if ( !newEntry.contains( oldAttribute ) )
                {
                    modRequest.addModification( new ClientModification( ModificationOperation.REPLACE_ATTRIBUTE,
                        new DefaultClientAttribute( oldAttribute.getId() ) ) );
                }
            }

            connection.modify( modRequest );
        }
        catch ( Exception e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            returnLdapConnection( connection );
        }
    }


    public void delete( T object )
    {
        LdapConnection connection = null;
        try
        {
            connection = getLdapConnection();
            Entry entry = getEntry( object );
            connection.delete( entry.getDn() );
        }
        catch ( Exception e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            returnLdapConnection( connection );
        }
    }


    private Entry getEntry( T object ) throws InvalidNameException, NamingException
    {
        Entry entry = new DefaultClientEntry();

        // map object to entry first, because this fills the RDN attribute
        mapToEntry( object, entry );

        DN dn = new DN( rdnAttribute, getString( entry, rdnAttribute ), parentDn );
        entry.setDn( dn );

        entry.add( "objectClass", objectClasses );

        return entry;
    }


    private Entry lookupEntry( LdapConnection connection, String dn ) throws LdapException
    {
        SearchResponse response = connection.lookup( dn, attributes );
        if ( response instanceof SearchResultEntry )
        {
            SearchResultEntry sre = ( SearchResultEntry ) response;
            Entry entry = sre.getEntry();
            return entry;
        }
        else
        {
            return null;
        }
    }


    protected abstract T mapFromEntry( Entry entry );


    protected abstract void mapToEntry( T object, Entry entry ) throws NamingException;


    protected byte[] getBytes( Entry entry, String attribute )
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return entryAttribute.get().getBytes();
        }
        else
        {
            return null;
        }
    }


    protected void setBytes( Entry entry, String attribute, byte[] value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, value );
        }
    }


    protected Integer getInt( Entry entry, String attribute )
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return Integer.parseInt( entryAttribute.get().getString() );
        }
        else
        {
            return null;
        }
    }


    protected void setInt( Entry entry, String attribute, Integer value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, value.toString() );
        }
    }


    protected void setCalendar( Entry entry, String attribute, Calendar value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, new GeneralizedTime( value ).toGeneralizedTime() );
        }
    }


    protected Calendar getCalendar( Entry entry, String attribute ) throws ParseException
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return new GeneralizedTime( attribute ).getCalendar();
        }
        else
        {
            return null;
        }
    }


    protected Long getLong( Entry entry, String attribute )
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return Long.parseLong( entryAttribute.get().getString() );
        }
        else
        {
            return null;
        }
    }


    protected void setLong( Entry entry, String attribute, Long value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, value.toString() );
        }
    }


    protected String getString( Entry entry, String attribute )
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return entryAttribute.get().getString();
        }
        else
        {
            return null;
        }
    }


    protected void setString( Entry entry, String attribute, String value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, value.toString() );
        }
    }


    protected void setBoolean( Entry entry, String attribute, Boolean value ) throws NamingException
    {
        if ( value != null )
        {
            entry.add( attribute, "TRUE" );
        }
        else
        {
            entry.add( attribute, "FALSE" );
        }
    }


    protected Boolean getBoolean( Entry entry, String attribute )
    {
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            return Boolean.parseBoolean( attribute );
        }
        else
        {
            return null;
        }
    }


    protected Set<String> getStringSet( Entry entry, String attribute )
    {
        Set<String> values = new HashSet<String>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( value.getString() );
            }
        }
        return values;
    }


    protected Set<Integer> getIntegerSet( Entry entry, String attribute )
    {
        Set<Integer> values = new HashSet<Integer>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( Integer.parseInt( value.getString() ) );
            }
        }
        return values;
    }


    protected Set<Long> getLongSet( Entry entry, String attribute )
    {
        Set<Long> values = new HashSet<Long>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( Long.parseLong( value.getString() ) );
            }
        }
        return values;
    }


    protected Set<Boolean> getBooleanSet( Entry entry, String attribute )
    {
        Set<Boolean> values = new HashSet<Boolean>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( Boolean.parseBoolean( value.getString() ) );
            }
        }
        return values;
    }


    protected Set<Calendar> getCalendarSet( Entry entry, String attribute ) throws ParseException
    {
        Set<Calendar> values = new HashSet<Calendar>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( new GeneralizedTime( value.getString() ).getCalendar() );
            }
        }
        return values;
    }


    protected Set<byte[]> getByteArraySet( Entry entry, String attribute )
    {
        Set<byte[]> values = new HashSet<byte[]>();
        EntryAttribute entryAttribute = entry.get( attribute );
        if ( entryAttribute != null )
        {
            for ( Value<?> value : entryAttribute )
            {
                values.add( value.getBytes() );
            }
        }
        return values;
    }


    protected LdapConnection getLdapConnection()
    {
        try
        {
            LdapConnection connection = new LdapConnection( server, port );
            connection.bind( name, credentials );
            return connection;
        }
        catch ( LdapException e )
        {
            throw new RuntimeException( e );
        }
        catch ( IOException e )
        {
            throw new RuntimeException( e );
        }
    }


    protected void returnLdapConnection( LdapConnection connection )
    {
        if ( connection != null )
        {
            try
            {
                connection.unBind();
                connection.close();
            }
            catch ( IOException e )
            {
                throw new RuntimeException( e );
            }
            catch ( Exception e )
            {
                throw new RuntimeException( e );
            }
        }
    }
}
