/**
 * 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.atlas.repository.memory;

import org.apache.atlas.AtlasException;
import org.apache.atlas.repository.DiscoverInstances;
import org.apache.atlas.repository.IRepository;
import org.apache.atlas.repository.RepositoryException;
import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.ITypedReferenceableInstance;
import org.apache.atlas.typesystem.persistence.Id;
import org.apache.atlas.typesystem.persistence.MapIds;
import org.apache.atlas.typesystem.persistence.ReferenceableInstance;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.DataTypes;
import org.apache.atlas.typesystem.types.HierarchicalType;
import org.apache.atlas.typesystem.types.HierarchicalTypeDependencySorter;
import org.apache.atlas.typesystem.types.Multiplicity;
import org.apache.atlas.typesystem.types.ObjectGraphWalker;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.atlas.typesystem.types.TypeSystem;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;

public class MemRepository implements IRepository {

    final TypeSystem typeSystem;
    /*
     * A Store for each Class and Trait.
     */
    final Map<String, HierarchicalTypeStore> typeStores;
    final AtomicInteger ID_SEQ = new AtomicInteger(0);

    public MemRepository(TypeSystem typeSystem) {
        this.typeSystem = typeSystem;
        this.typeStores = new HashMap<>();
    }

    @Override
    public Id newId(String typeName) {
        return new Id("" + ID_SEQ.incrementAndGet(), 0, typeName);
    }

    /**
     * 1. traverse the Object Graph from  i and create idToNewIdMap : Map[Id, Id],
     *    also create old Id to Instance Map: oldIdToInstance : Map[Id, IInstance]
     *   - traverse reference Attributes, List[ClassType], Maps where Key/value is ClassType
     *   - traverse Structs
     *   - traverse Traits.
     * 1b. Ensure that every newId has an associated Instance.
     * 2. Traverse oldIdToInstance map create newInstances : List[ITypedReferenceableInstance]
     *    - create a ITypedReferenceableInstance.
     *      replace any old References ( ids or object references) with new Ids.
     * 3. Traverse over newInstances
     *    - ask ClassStore to assign a position to the Id.
     *      - for Instances with Traits, assign a position for each Trait
     *    - invoke store on the nwInstance.
     *
     * Recovery:
     * - on each newInstance, invoke releaseId and delete on its ClassStore and Traits' Stores.
     *
     * @param i
     * @return
     * @throws org.apache.atlas.repository.RepositoryException
     */
    public ITypedReferenceableInstance create(IReferenceableInstance i) throws RepositoryException {

        DiscoverInstances discoverInstances = new DiscoverInstances(this);

        /*
         * Step 1: traverse the Object Graph from  i and create idToNewIdMap : Map[Id, Id],
         *    also create old Id to Instance Map: oldIdToInstance : Map[Id, IInstance]
         *   - traverse reference Attributes, List[ClassType], Maps where Key/value is ClassType
         *   - traverse Structs
         *   - traverse Traits.
         */
        try {
            new ObjectGraphWalker(typeSystem, discoverInstances, i).walk();
        } catch (AtlasException me) {
            throw new RepositoryException("TypeSystem error when walking the ObjectGraph", me);
        }

        /*
         * Step 1b: Ensure that every newId has an associated Instance.
         */
        for (Id oldId : discoverInstances.idToNewIdMap.keySet()) {
            if (!discoverInstances.idToInstanceMap.containsKey(oldId)) {
                throw new RepositoryException(String.format("Invalid Object Graph: "
                                + "Encountered an unassignedId %s that is not associated with an Instance", oldId));
            }
        }

        /* Step 2: Traverse oldIdToInstance map create newInstances :
        List[ITypedReferenceableInstance]
         * - create a ITypedReferenceableInstance.
         *   replace any old References ( ids or object references) with new Ids.
        */
        List<ITypedReferenceableInstance> newInstances = new ArrayList<ITypedReferenceableInstance>();
        ITypedReferenceableInstance retInstance = null;
        Set<ClassType> classTypes = new TreeSet<ClassType>();
        Set<TraitType> traitTypes = new TreeSet<TraitType>();
        for (IReferenceableInstance transientInstance : discoverInstances.idToInstanceMap.values()) {
            try {
                ClassType cT = typeSystem.getDataType(ClassType.class, transientInstance.getTypeName());
                ITypedReferenceableInstance newInstance = cT.convert(transientInstance, Multiplicity.REQUIRED);
                newInstances.add(newInstance);

                classTypes.add(cT);
                for (String traitName : newInstance.getTraits()) {
                    TraitType tT = typeSystem.getDataType(TraitType.class, traitName);
                    traitTypes.add(tT);
                }

                if (newInstance.getId() == i.getId()) {
                    retInstance = newInstance;
                }

                /*
                 * Now replace old references with new Ids
                 */
                MapIds mapIds = new MapIds(discoverInstances.idToNewIdMap);
                new ObjectGraphWalker(typeSystem, mapIds, newInstances).walk();

            } catch (AtlasException me) {
                throw new RepositoryException(
                        String.format("Failed to create Instance(id = %s", transientInstance.getId()), me);
            }
        }

        /*
         * 3. Acquire Class and Trait Storage locks.
         * - acquire them in a stable order (super before subclass, classes before traits
         */
        for (ClassType cT : classTypes) {
            HierarchicalTypeStore st = typeStores.get(cT.getName());
            st.acquireWriteLock();
        }

        for (TraitType tT : traitTypes) {
            HierarchicalTypeStore st = typeStores.get(tT.getName());
            st.acquireWriteLock();
        }


        /*
         * 4. Traverse over newInstances
         *    - ask ClassStore to assign a position to the Id.
         *      - for Instances with Traits, assign a position for each Trait
         *    - invoke store on the nwInstance.
         */
        try {
            for (ITypedReferenceableInstance instance : newInstances) {
                HierarchicalTypeStore st = typeStores.get(instance.getTypeName());
                st.assignPosition(instance.getId());
                for (String traitName : instance.getTraits()) {
                    HierarchicalTypeStore tt = typeStores.get(traitName);
                    tt.assignPosition(instance.getId());
                }
            }

            for (ITypedReferenceableInstance instance : newInstances) {
                HierarchicalTypeStore st = typeStores.get(instance.getTypeName());
                st.store((ReferenceableInstance) instance);
                for (String traitName : instance.getTraits()) {
                    HierarchicalTypeStore tt = typeStores.get(traitName);
                    tt.store((ReferenceableInstance) instance);
                }
            }
        } catch (RepositoryException re) {
            for (ITypedReferenceableInstance instance : newInstances) {
                HierarchicalTypeStore st = typeStores.get(instance.getTypeName());
                st.releaseId(instance.getId());
            }
            throw re;
        } finally {
            for (ClassType cT : classTypes) {
                HierarchicalTypeStore st = typeStores.get(cT.getName());
                st.releaseWriteLock();
            }

            for (TraitType tT : traitTypes) {
                HierarchicalTypeStore st = typeStores.get(tT.getName());
                st.releaseWriteLock();
            }
        }

        return retInstance;
    }

    public ITypedReferenceableInstance update(ITypedReferenceableInstance i) throws RepositoryException {
        throw new RepositoryException("not implemented");
    }

    public void delete(ITypedReferenceableInstance i) throws RepositoryException {
        throw new RepositoryException("not implemented");
    }

    public ITypedReferenceableInstance get(Id id) throws RepositoryException {

        try {
            ReplaceIdWithInstance replacer = new ReplaceIdWithInstance(this);
            ObjectGraphWalker walker = new ObjectGraphWalker(typeSystem, replacer);
            replacer.setWalker(walker);
            ITypedReferenceableInstance r = getDuringWalk(id, walker);
            walker.walk();
            return r;
        } catch (AtlasException me) {
            throw new RepositoryException("TypeSystem error when walking the ObjectGraph", me);
        }
    }

    /*
     * - Id must be valid; Class must be valid.
     * - Ask ClassStore to createInstance.
     * - Ask ClassStore to load instance.
     * - load instance traits
     * - add to GraphWalker
     */
    ITypedReferenceableInstance getDuringWalk(Id id, ObjectGraphWalker walker) throws RepositoryException {
        ClassStore cS = getClassStore(id.getTypeName());
        if (cS == null) {
            throw new RepositoryException(String.format("Unknown Class %s", id.getTypeName()));
        }
        cS.validate(this, id);
        ReferenceableInstance r = cS.createInstance(this, id);
        cS.load(r);
        for (String traitName : r.getTraits()) {
            HierarchicalTypeStore tt = typeStores.get(traitName);
            tt.load(r);
        }

        walker.addRoot(r);
        return r;
    }

    HierarchicalTypeStore getStore(String typeName) {
        return typeStores.get(typeName);
    }

    ClassStore getClassStore(String typeName) {
        return (ClassStore) getStore(typeName);
    }

    public void defineClass(ClassType type) throws RepositoryException {
        HierarchicalTypeStore s = new ClassStore(this, type);
        typeStores.put(type.getName(), s);
    }

    public void defineTrait(TraitType type) throws RepositoryException {
        HierarchicalTypeStore s = new TraitStore(this, type);
        typeStores.put(type.getName(), s);
    }

    public void defineTypes(List<HierarchicalType> types) throws RepositoryException {
        List<TraitType> tTypes = new ArrayList<TraitType>();
        List<ClassType> cTypes = new ArrayList<ClassType>();

        for (HierarchicalType h : types) {
            if (h.getTypeCategory() == DataTypes.TypeCategory.TRAIT) {
                tTypes.add((TraitType) h);
            } else {
                cTypes.add((ClassType) h);
            }
        }
        tTypes = HierarchicalTypeDependencySorter.sortTypes(tTypes);
        cTypes = HierarchicalTypeDependencySorter.sortTypes(cTypes);

        for (TraitType tT : tTypes) {
            defineTrait(tT);
        }

        for (ClassType cT : cTypes) {
            defineClass(cT);
        }
    }
}
