package org.drools.base;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.drools.RuntimeDroolsException;
import org.drools.base.AccessorKey.AccessorType;
import org.drools.spi.Acceptor;
import org.drools.spi.AcceptsClassObjectType;
import org.drools.spi.AcceptsReadAccessor;
import org.drools.spi.AcceptsWriteAccessor;

public class ClassFieldAccessorStore
    implements
    Externalizable {
    private Map<AccessorKey, BaseLookupEntry> lookup;

    private ClassFieldAccessorCache           cache;

    /**
     * This field is just there to assist in testing
     */
    private boolean                           eagerWire = true;

    public ClassFieldAccessorStore() {
        lookup = new HashMap<AccessorKey, BaseLookupEntry>();
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject( lookup );
    }

    public void readExternal(ObjectInput in) throws IOException,
                                            ClassNotFoundException {
        lookup = (Map<AccessorKey, BaseLookupEntry>) in.readObject();
    }

    public void setEagerWire(boolean eagerWire) {
        this.eagerWire = eagerWire;
    }

    public void setClassFieldAccessorCache(ClassFieldAccessorCache cache) {
        this.cache = cache;
    }

    public ClassFieldWriter getWriter(Class cls,
                                      String fieldName,
                                      ClassLoader classLoader) {
        return getWriter( cls.getName(),
                          fieldName,
                          null );
    }

    public ClassFieldReader getReader(Class cls,
                                      String fieldName,
                                      ClassLoader classLoader) {
        return getReader( cls.getName(),
                          fieldName,
                          null,
                          AccessorKey.AccessorType.FieldAccessor );
    }

    public ClassFieldReader getReader(final String className,
                                      final String fieldName,
                                      final AcceptsReadAccessor target) {
        return getReader( className,
                          fieldName,
                          target,
                          AccessorKey.AccessorType.FieldAccessor );
    }

    public synchronized ClassFieldReader getReader(final String className,
                                                   final String fieldName,
                                                   final AcceptsReadAccessor target,
                                                   final AccessorKey.AccessorType accessorType) {
        AccessorKey key = new AccessorKey( className,
                                           fieldName,
                                           accessorType );
        FieldLookupEntry entry = (FieldLookupEntry) this.lookup.get( key );
        
        boolean exists = true;
        if ( entry == null ) {
            exists = false;
            entry = new FieldLookupEntry( new ClassFieldReader( className,
                                                                fieldName ) );
        }

        if ( this.eagerWire ) {
            wire( entry.getClassFieldReader() );
        }        
        

        if ( target != null ) {
            target.setReadAccessor( entry.getClassFieldReader() );
            entry.addAccessorTarget( target );
        }
        
        if( exists == false ) {
            // we delay the key writing as we only want to do it if the wiring was successful
            this.lookup.put( key,
                             entry );   
        }

        return entry.getClassFieldReader();
    }

    public synchronized ClassFieldWriter getWriter(final String className,
                                                   final String fieldName,
                                                   final AcceptsWriteAccessor target) {
        AccessorKey key = new AccessorKey( className,
                                           fieldName,
                                           AccessorKey.AccessorType.FieldAccessor );
        FieldLookupEntry entry = (FieldLookupEntry) this.lookup.get( key );
        boolean exists = true;
        if ( entry == null ) {
            exists = false;
            entry = new FieldLookupEntry( new ClassFieldWriter( className,
                                                                fieldName ) );
        }

        if ( this.eagerWire ) {
            wire( entry.getClassFieldReader() );
        }

        if ( target != null ) {
            target.setWriteAccessor( entry.getClassFieldWriter() );
            entry.addAccessorTarget( target );
        }

        if( exists == false ) {
            // we delay the key writing as we only want to do it if the wiring was successful
            this.lookup.put( key,
                             entry );   
        }        
        
        return entry.getClassFieldWriter();
    }

    public ClassFieldAccessor getAccessor(Class cls,
                                          String fieldName,
                                          ClassLoader classLoader) {
        return getAccessor( cls.getName(),
                            fieldName );
    }

    public ClassFieldAccessor getAccessor(final String className,
                                          final String fieldName) {
        AccessorKey key = new AccessorKey( className,
                                           fieldName,
                                           AccessorKey.AccessorType.FieldAccessor );
        FieldLookupEntry entry = (FieldLookupEntry) this.lookup.get( key );
        if ( entry == null ) {
            entry = new FieldLookupEntry( new ClassFieldReader( className,
                                                                fieldName ),
                                          new ClassFieldWriter( className,
                                                                fieldName ) );
            this.lookup.put( key,
                             entry );
        }

        ClassFieldAccessor accessor = new ClassFieldAccessor( entry.getClassFieldReader(),
                                                              entry.getClassFieldWriter() );

        entry.addAccessorTarget( accessor );

        if ( this.eagerWire ) {
            wire( entry.getClassFieldReader() );
            wire( entry.getClassFieldWriter() );
        }

        return accessor;
    }

    //    public PatternExtractor getObjectAccessor(final Class cls,
    //                                              final String identifier,
    //                                              final Declaration declaration) {
    //        AccessorKey key = new AccessorKey( cls.getName(),
    //                                           identifier, // we are re-using the fieldName as a global identifier
    //                                           AccessorKey.AccessorType.GlobalAccessor );
    //
    //        ObjectExtractorLookupEntry entry = (ObjectExtractorLookupEntry) this.lookup.get( key );
    //        if ( entry == null ) {
    //            PatternExtractor extractor = (PatternExtractor) declaration.getExtractor();
    //            entry = new ObjectExtractorLookupEntry( extractor );
    //            this.lookup.put( key,
    //                             entry );
    //        }
    //
    //        entry.addAccessorTarget( declaration );
    //
    //        // there is no wiring here as the GlobalExtractor already references the class, 
    //        // although we will need to re-wire on serialisation 
    //
    //        return entry.getObjectExtractor();
    //    }
    //
    //    public GlobalExtractor getGlobalAccessor(final ClassObjectType classObjectType,
    //                                             final String identifier,
    //                                             final Declaration declaration) {
    //        AccessorKey key = new AccessorKey( classObjectType.getClassType().getName(),
    //                                           identifier, // we are re-using the fieldName as a global identifier
    //                                           AccessorKey.AccessorType.GlobalAccessor );
    //
    //        GlobalExtractorLookupEntry entry = (GlobalExtractorLookupEntry) this.lookup.get( key );
    //        if ( entry == null ) {
    //            entry = new GlobalExtractorLookupEntry( new GlobalExtractor( identifier,
    //                                                                         classObjectType ) );
    //            this.lookup.put( key,
    //                             entry );
    //        }
    //
    //        entry.addAccessorTarget( declaration );
    //        declaration.setReadAccessor( entry.getGlobalExtractor() );
    //
    //        // there is no wiring here as the GlobalExtractor already references the class, 
    //        // although we will need to re-wire on serialisation 
    //
    //        return entry.getGlobalExtractor();
    //    }

    public ClassObjectType getClassObjectType(final ClassObjectType objectType,
                                              final AcceptsClassObjectType target) {
        return getClassObjectType( objectType,
                                   false,
                                   target );
    }

    public ClassObjectType getClassObjectType(final ClassObjectType objectType,
                                              final boolean isEvent,
                                              final AcceptsClassObjectType target) {
        AccessorKey key = new AccessorKey( objectType.getClassName(),
                                           isEvent ? "$$DROOLS__isEvent__" : null,
                                           AccessorKey.AccessorType.ClassObjectType );

        ClassObjectTypeLookupEntry entry = (ClassObjectTypeLookupEntry) this.lookup.get( key );
        if ( entry == null ) {
            entry = new ClassObjectTypeLookupEntry(  cache.getClassObjectType( objectType ) );
            this.lookup.put( key,
                             entry );
        }

        if ( target != null ) {
            entry.addAccessorTarget( target );
            target.setClassObjectType( entry.getClassObjectType() );
        }

        return entry.getClassObjectType();
    }

    public void merge(ClassFieldAccessorStore other) {
        for ( Entry<AccessorKey, BaseLookupEntry> entry : other.lookup.entrySet() ) {

            switch ( entry.getValue().getAccessorType() ) {
                case FieldAccessor : {
                    FieldLookupEntry lookupEntry = (FieldLookupEntry) this.lookup.get( entry.getKey() );
                    if ( lookupEntry == null ) {
                        lookupEntry = (FieldLookupEntry) entry.getValue();
                        this.lookup.put( entry.getKey(),
                                         lookupEntry );
                        // wire up ClassFieldReaders
                        if ( lookupEntry.getClassFieldReader() != null ) {
                            wire( lookupEntry.getClassFieldReader() );
                        }

                        if ( lookupEntry.getClassFieldWriter() != null ) {
                            wire( lookupEntry.getClassFieldWriter() );
                        }
                    } else {
                        // iterate through new targets adding them and wiring them up
                        // to the existing ClassFieldReader, no need to wire generated accessor
                        // as we know it already exists
                        for ( Acceptor target : entry.getValue().getAccessorTargets() ) {
                            if ( target instanceof AcceptsReadAccessor ) {
                                ((AcceptsReadAccessor) target).setReadAccessor( lookupEntry.getClassFieldReader() );
                            } else if ( target instanceof AcceptsWriteAccessor ) {
                                ((AcceptsWriteAccessor) target).setWriteAccessor( lookupEntry.getClassFieldWriter() );
                            }
                            lookupEntry.addAccessorTarget( target );
                        }
                    }
                    break;
                }

                case ClassObjectType : {
                    ClassObjectTypeLookupEntry lookupEntry = (ClassObjectTypeLookupEntry) this.lookup.get( entry.getKey() );
                    if ( lookupEntry == null ) {
                                                // Create new entry with correct ClassObjectType and targets
                        lookupEntry = new ClassObjectTypeLookupEntry(  cache.getClassObjectType( ((ClassObjectTypeLookupEntry) entry.getValue()).getClassObjectType() ) );                        
                        
                        this.lookup.put( entry.getKey(),
                                         lookupEntry );
                                               
                    } 
                    
                    for ( Acceptor target : entry.getValue().getAccessorTargets() ) {
                        ((AcceptsClassObjectType) target).setClassObjectType( lookupEntry.getClassObjectType() );                            
                        lookupEntry.addAccessorTarget( target );
                    }
                }

                    //                case ObjectAccessor : {
                    //                    ObjectExtractorLookupEntry lookupEntry = ( ObjectExtractorLookupEntry ) this.lookup.get( entry.getKey() );
                    //                    if ( lookupEntry == null ) {
                    //                        lookupEntry = ( ObjectExtractorLookupEntry )  entry.getValue();
                    //                        this.lookup.put( entry.getKey(),
                    //                                         lookupEntry );
                    //                        wire( lookupEntry.getObjectExtractor() );                       
                    //                    } else {
                    //                        for ( Acceptor target : entry.getValue().getAccessorTargets() ) {
                    //                            ((Declaration)target).setReadAccessor( lookupEntry.getObjectExtractor() );
                    //                            lookupEntry.addAccessorTarget( target );
                    //                        }
                    //                    }
                    //                    break;
                    //                }
                    //                case GlobalAccessor : {
                    //                    GlobalExtractorLookupEntry lookupEntry = ( GlobalExtractorLookupEntry ) this.lookup.get( entry.getKey() );
                    //                    if ( lookupEntry == null ) {
                    //                        lookupEntry = ( GlobalExtractorLookupEntry )  entry.getValue();
                    //                        this.lookup.put( entry.getKey(),
                    //                                         lookupEntry );
                    //                        wire( lookupEntry.getGlobalExtractor() );  
                    //                    } else {
                    //                        for ( Acceptor target : entry.getValue().getAccessorTargets() ) {
                    //                            ((Declaration)target).setReadAccessor( lookupEntry.getGlobalExtractor() );
                    //                            lookupEntry.addAccessorTarget( target );
                    //                        }                        
                    //                    }                    
                    //                    break;
                    //                }
            }
        }
    }

    public void wire() {
        for ( Entry<AccessorKey, BaseLookupEntry> entry : lookup.entrySet() ) {
            switch ( entry.getValue().getAccessorType() ) {
                case FieldAccessor : {
                    ClassFieldReader reader = ((FieldLookupEntry) entry.getValue()).getClassFieldReader();
                    if ( reader != null ) {
                        wire( reader );
                    }

                    ClassFieldWriter writer = ((FieldLookupEntry) entry.getValue()).getClassFieldWriter();
                    if ( writer != null ) {
                        wire( writer );
                    }
                    break;
                }
                case ClassObjectType : {
                    ClassObjectType classObjectType = ((ClassObjectTypeLookupEntry) entry.getValue()).getClassObjectType();
                    wire( classObjectType );
                    break;
                }

                    //                case ObjectAccessor : {
                    //                    PatternExtractor reader = ((ObjectExtractorLookupEntry) entry.getValue()).getObjectExtractor();
                    //                    wire( reader );
                    //                    break;
                    //                }
                    //                case GlobalAccessor : {
                    //                    GlobalExtractor reader = ((GlobalExtractorLookupEntry) entry.getValue()).getGlobalExtractor();
                    //                    wire( reader );
                    //                    break;
                    //                }
            }
        }
    }

    public void wire(ClassFieldReader reader) {
        reader.setReadAccessor( cache.getReadAcessor( reader ) );
    }

    public void wire(ClassFieldWriter writer) {
        writer.setWriteAccessor( cache.getWriteAcessor( writer ) );
    }

    //    public void wire(PatternExtractor reader) {
    //        ObjectType objectType = reader.getObjectType();
    //
    //        if ( objectType instanceof ClassObjectType ) {
    //            ClassObjectType cot = (ClassObjectType) objectType;
    //            try {
    //                Class cls = this.cache.getClassLoader().loadClass( cot.getClassName() );
    //                cot.setClassType( cls );
    //            } catch ( ClassNotFoundException e ) {
    //                throw new RuntimeDroolsException( "Unable to load ClassObjectType class '" + cot.getClassName() + "'" );
    //            }
    //        }
    //    }    

    public void wire(ClassObjectType objectType) {
        try {
            Class cls = this.cache.getClassLoader().loadClass( objectType.getClassName() );
            objectType.setClassType( cls );
        } catch ( ClassNotFoundException e ) {
            throw new RuntimeDroolsException( "Unable to load ClassObjectType class '" + objectType.getClassName() + "'" );
        }
    }

    public static abstract class BaseLookupEntry
        implements
        Externalizable {
        // we use an identity hashmap to avoid hashcode/equals being called on stored targets
        private Map<Acceptor, Object> accessorTargets = Collections.<Acceptor, Object> emptyMap();

        public BaseLookupEntry() {

        }

        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject( accessorTargets );
        }

        public void readExternal(ObjectInput in) throws IOException,
                                                ClassNotFoundException {
            accessorTargets = (Map<Acceptor, Object>) in.readObject();

        }

        public Set<Acceptor> getAccessorTargets() {
            return accessorTargets.keySet();
        }

        public void addAccessorTarget(Acceptor target) {
            if ( this.accessorTargets == Collections.EMPTY_MAP ) {
                this.accessorTargets = new IdentityHashMap<Acceptor, Object>();
            }

            this.accessorTargets.put( target, null );
        }
        
        public void addAccessorTargets(Set<Acceptor> targets) {
            if ( this.accessorTargets == Collections.EMPTY_MAP ) {
                this.accessorTargets = new IdentityHashMap<Acceptor, Object>( );
            }
            
            for ( Acceptor target : targets ) {
                this.accessorTargets.put( target, null );
            }
        }

        public void removeTarget(Acceptor target) {
            this.accessorTargets.remove( target );
        }

        public abstract AccessorKey.AccessorType getAccessorType();

    }

    public static class ClassObjectTypeLookupEntry extends BaseLookupEntry {
        ClassObjectType classObjectType;

        public ClassObjectTypeLookupEntry() {
            super();
        }

        public ClassObjectTypeLookupEntry(ClassObjectType classObjectType) {
            super();
            this.classObjectType = classObjectType;
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal( out );
            out.writeObject( classObjectType );
        }

        public void readExternal(ObjectInput in) throws IOException,
                                                ClassNotFoundException {
            super.readExternal( in );
            classObjectType = (ClassObjectType) in.readObject();
        }

        public ClassObjectType getClassObjectType() {
            return classObjectType;
        }        
        
        public void setClassObjectType(ClassObjectType classObjectType) {
            this.classObjectType = classObjectType;
        }

        public AccessorType getAccessorType() {
            return AccessorKey.AccessorType.ClassObjectType;
        }

    }

    //
    //    public static class GlobalExtractorLookupEntry extends BaseLookupEntry {
    //        GlobalExtractor globalExtractor;
    //
    //        public GlobalExtractorLookupEntry() {
    //            super();
    //        }
    //
    //        public GlobalExtractorLookupEntry(GlobalExtractor globalExtractor) {
    //            super();
    //            this.globalExtractor = globalExtractor;
    //        }
    //
    //        public void writeExternal(ObjectOutput out) throws IOException {
    //            super.writeExternal( out );
    //            out.writeObject( globalExtractor );
    //        }
    //
    //        public void readExternal(ObjectInput in) throws IOException,
    //                                                ClassNotFoundException {
    //            super.readExternal( in );
    //            globalExtractor = (GlobalExtractor) in.readObject();
    //        }
    //
    //        public GlobalExtractor getGlobalExtractor() {
    //            return globalExtractor;
    //        }
    //
    //        public AccessorType getAccessorType() {
    //            return AccessorKey.AccessorType.GlobalAccessor;
    //        }
    //
    //    }
    //
    //    public static class ObjectExtractorLookupEntry extends BaseLookupEntry {
    //        PatternExtractor patternExtractor;
    //
    //        public ObjectExtractorLookupEntry() {
    //            super();
    //        }
    //
    //        public ObjectExtractorLookupEntry(PatternExtractor patternExtractor) {
    //            super();
    //            this.patternExtractor = patternExtractor;
    //        }
    //
    //        public void writeExternal(ObjectOutput out) throws IOException {
    //            super.writeExternal( out );
    //            out.writeObject( patternExtractor );
    //        }
    //
    //        public void readExternal(ObjectInput in) throws IOException,
    //                                                ClassNotFoundException {
    //            super.readExternal( in );
    //            patternExtractor = (PatternExtractor) in.readObject();
    //        }
    //
    //        public PatternExtractor getObjectExtractor() {
    //            return patternExtractor;
    //        }
    //
    //        public AccessorType getAccessorType() {
    //            return AccessorKey.AccessorType.ObjectAccessor;
    //        }
    //    }
    //
    public static class FieldLookupEntry extends BaseLookupEntry {
        private ClassFieldReader reader;
        private ClassFieldWriter writer;

        public FieldLookupEntry() {

        }

        public FieldLookupEntry(ClassFieldReader reader) {
            this.reader = reader;
        }

        public FieldLookupEntry(ClassFieldWriter writer) {
            this.writer = writer;
        }

        public FieldLookupEntry(ClassFieldReader reader,
                                ClassFieldWriter writer) {
            this.writer = writer;
            this.reader = reader;
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal( out );
            out.writeObject( reader );
            out.writeObject( writer );
        }

        public void readExternal(ObjectInput in) throws IOException,
                                                ClassNotFoundException {
            super.readExternal( in );
            reader = (ClassFieldReader) in.readObject();
            writer = (ClassFieldWriter) in.readObject();
        }

        public ClassFieldReader getClassFieldReader() {
            return reader;
        }

        public ClassFieldWriter getClassFieldWriter() {
            return this.writer;
        }

        public AccessorType getAccessorType() {
            return AccessorKey.AccessorType.FieldAccessor;
        }
    }

    //    public static class LookupEntry
    //        implements
    //        Externalizable {
    //        private ClassFieldReader     reader;
    //        private ClassFieldWriter     writer;
    //        private Set<AcceptsAccessor> accessorTargets = Collections.<AcceptsAccessor> emptySet();
    //
    //        public LookupEntry() {
    //
    //        }
    //
    //        public void writeExternal(ObjectOutput out) throws IOException {
    //            out.writeObject( reader );
    //            out.writeObject( writer );
    //            out.writeObject( accessorTargets );
    //        }
    //
    //        public void readExternal(ObjectInput in) throws IOException,
    //                                                ClassNotFoundException {
    //            reader = (ClassFieldReader) in.readObject();
    //            writer = (ClassFieldWriter) in.readObject();
    //            accessorTargets = (Set<AcceptsAccessor>) in.readObject();
    //
    //        }
    //
    //        public LookupEntry(ClassFieldReader reader) {
    //            this.reader = reader;
    //        }
    //
    //        public LookupEntry(ClassFieldWriter writer) {
    //            this.writer = writer;
    //        }
    //
    //        public LookupEntry(ClassFieldReader reader,
    //                           ClassFieldWriter writer) {
    //            this.writer = writer;
    //            this.reader = reader;
    //        }
    //
    //        public ClassFieldReader getClassFieldReader() {
    //            return reader;
    //        }
    //
    //        public ClassFieldWriter getClassFieldWriter() {
    //            return this.writer;
    //        }
    //
    //        public Set<AcceptsAccessor> getAccessorTargets() {
    //            return accessorTargets;
    //        }
    //
    //        public void addAccessorTarget(AcceptsAccessor target) {
    //            if ( this.accessorTargets == Collections.EMPTY_SET ) {
    //                this.accessorTargets = new HashSet<AcceptsAccessor>();
    //            }
    //
    //            this.accessorTargets.add( target );
    //        }
    //
    //        public void removeConstraint(Constraint constraint) {
    //            this.accessorTargets.remove( constraint );
    //        }
    //    }
}
