/**
 * 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.typesystem.types;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.persistence.Id;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

public class ObjectGraphTraversal implements Iterator<ObjectGraphTraversal.InstanceTuple> {

    final Queue<InstanceTuple> queue;
    final TypeSystem typeSystem;
    Set<Id> processedIds;

    public ObjectGraphTraversal(TypeSystem typeSystem, IReferenceableInstance start) throws AtlasException {
        this.typeSystem = typeSystem;
        queue = new LinkedList<>();
        processedIds = new HashSet<>();
        processReferenceableInstance(start);
    }

    void processValue(IDataType dT, Object val) throws AtlasException {
        if (val != null) {
            if (dT.getTypeCategory() == DataTypes.TypeCategory.ARRAY) {
                IDataType elemType = ((DataTypes.ArrayType) dT).getElemType();
                processCollection(elemType, val);
            } else if (dT.getTypeCategory() == DataTypes.TypeCategory.MAP) {
                IDataType keyType = ((DataTypes.MapType) dT).getKeyType();
                IDataType valueType = ((DataTypes.MapType) dT).getValueType();
                processMap(keyType, valueType, val);
            } else if (dT.getTypeCategory() == DataTypes.TypeCategory.STRUCT
                    || dT.getTypeCategory() == DataTypes.TypeCategory.TRAIT) {
                processStruct(val);
            } else if (dT.getTypeCategory() == DataTypes.TypeCategory.CLASS) {
                processReferenceableInstance(val);
            }
        }
    }

    void processMap(IDataType keyType, IDataType valueType, Object val) throws AtlasException {
        if (keyType.getTypeCategory() == DataTypes.TypeCategory.PRIMITIVE
                && valueType.getTypeCategory() == DataTypes.TypeCategory.PRIMITIVE) {
            return;
        }

        if (val != null) {
            Iterator<Map.Entry> it = null;
            if (Map.class.isAssignableFrom(val.getClass())) {
                it = ((Map) val).entrySet().iterator();
                ImmutableMap.Builder b = ImmutableMap.builder();
                while (it.hasNext()) {
                    Map.Entry e = it.next();
                    processValue(keyType, e.getKey());
                    processValue(valueType, e.getValue());
                }
            }
        }
    }

    void processCollection(IDataType elemType, Object val) throws AtlasException {

        if (elemType.getTypeCategory() == DataTypes.TypeCategory.PRIMITIVE) {
            return;
        }

        if (val != null) {
            Iterator it = null;
            if (val instanceof Collection) {
                it = ((Collection) val).iterator();
            } else if (val instanceof Iterable) {
                it = ((Iterable) val).iterator();
            } else if (val instanceof Iterator) {
                it = (Iterator) val;
            }
            if (it != null) {
                DataTypes.TypeCategory elemCategory = elemType.getTypeCategory();
                while (it.hasNext()) {
                    Object elem = it.next();
                    processValue(elemType, elem);
                }
            }
        }
    }

    void processStruct(Object val) throws AtlasException {

        if (val == null || !(val instanceof IStruct)) {
            return;
        }

        IStruct i = (IStruct) val;

        IConstructableType type = typeSystem.getDataType(IConstructableType.class, i.getTypeName());

        for (Map.Entry<String, AttributeInfo> e : type.fieldMapping().fields.entrySet()) {
            AttributeInfo aInfo = e.getValue();
            String attrName = e.getKey();
            if (aInfo.dataType().getTypeCategory() != DataTypes.TypeCategory.PRIMITIVE) {
                processValue(aInfo.dataType(), i.get(attrName));
            }
        }
    }

    void processReferenceableInstance(Object val) throws AtlasException {

        if (val == null || !(val instanceof IReferenceableInstance || val instanceof Id)) {
            return;
        }

        if (val instanceof Id) {
            Id id = (Id) val;
            if (id.isUnassigned()) {
                add(id, null);
            }
            return;
        }

        IReferenceableInstance ref = (IReferenceableInstance) val;
        Id id = ref.getId();
        if (id.isUnassigned()) {
            add(id, ref);
            if (!processedIds.contains(id)) {
                processedIds.add(id);
                processStruct(val);

                ImmutableList<String> traits = ref.getTraits();
                for (String trait : traits) {
                    processStruct(ref.getTrait(trait));
                }
            }
        }
    }

    void add(Id id, IReferenceableInstance ref) {
        queue.add(new InstanceTuple(id, ref));
    }


    @Override
    public boolean hasNext() {
        return !queue.isEmpty();
    }

    @Override
    public InstanceTuple next() {
        try {
            InstanceTuple t = queue.poll();
            if(t != null) {
                processReferenceableInstance(t.instance);
            }
            return t;
        } catch (AtlasException me) {
            throw new RuntimeException(me);
        }
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    public static class InstanceTuple {
        public final Id id;
        public final IReferenceableInstance instance;

        public InstanceTuple(Id id, IReferenceableInstance instance) {
            this.id = id;
            this.instance = instance;
        }
    }

}
