/*
 *  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.isis.core.runtime.fixturedomainservice;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.Maps;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils;
import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.ObjectSpecificationException;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
import org.apache.isis.core.runtime.system.persistence.PersistenceSession;
import org.apache.isis.core.runtime.system.session.IsisSessionFactory;

public class ObjectFixtureFilePersistor {

    private static final Logger LOG = LoggerFactory.getLogger(ObjectFixtureFilePersistor.class);

    public Set<Object> loadData(final Reader reader) throws IOException {
        final Set<Object> objects = new HashSet<Object>();

        final BufferedReader buffer = new BufferedReader(reader);
        final LoadedObjects loaded = new LoadedObjects(objects, specificationLoader, getPersistenceSession());
        String line;
        ObjectAdapter object = null;
        int lineNo = 0;
        try {
            while ((line = buffer.readLine()) != null) {
                lineNo++;
                if (line.trim().startsWith("#")) {
                    continue;
                } else if (line.startsWith("  ")) {
                    loadFieldData(object, loaded, line);
                } else {
                    if (object != null && !object.representsPersistent()) {
                        getPersistenceSession().makePersistentInTransaction(object);
                    }
                    object = loaded.get(line);
                }
            }

            if (object != null && !object.representsPersistent()) {
                getPersistenceSession().makePersistentInTransaction(object);
            }
        } catch (final Exception e) {
            throw new FixtureException("failed to load data at line " + lineNo, e);
        }

        return objects;
    }

    private void loadFieldData(final ObjectAdapter object, final LoadedObjects loaded, final String line) {
        final int pos = line.indexOf(':');
        if (pos == -1) {
            throw new FixtureException("no colon (:) in: " + line.trim());
        }
        final String name = line.substring(0, pos).trim();
        String data = line.substring(pos + 1).trim();
        try {
            final ObjectAssociation association = object.getSpecification().getAssociation(name);
            if (data.trim().length() == 0) {
                if (    association instanceof OneToOneAssociation &&
                        !association.isEmpty(object, InteractionInitiatedBy.FRAMEWORK)) {
                    final OneToOneAssociation property = (OneToOneAssociation) association;
                    property.set(object, null, InteractionInitiatedBy.FRAMEWORK);
                }
            } else {
                if (association.isOneToManyAssociation()) {
                    final String[] ids = data.split(" ");
                    final ObjectAdapter[] elements = new ObjectAdapter[ids.length];
                    for (int i = 0; i < ids.length; i++) {
                        elements[i] = loaded.get(ids[i]);
                    }
                    final ObjectAdapter collection = association.get(object, InteractionInitiatedBy.FRAMEWORK);
                    final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(collection);
                    facet.init(collection, elements);
                } else if (association.getSpecification().isParseable()) {
                    data = data.replaceAll("\\n", "\n");
                    final ParseableFacet facet = association.getSpecification().getFacet(ParseableFacet.class);
                    final ObjectAdapter value = facet.parseTextEntry(null, data, InteractionInitiatedBy.FRAMEWORK);
                    ((OneToOneAssociation) association).initAssociation(object, value);
                } else if (association.isOneToOneAssociation()) {
                    final ObjectAdapter value = loaded.get(data);
                    ((OneToOneAssociation) association).initAssociation(object, value);
                }
            }
        } catch (final ObjectSpecificationException e) {
            LOG.info("no field for '{}', skipping entry: {}", name, data);
        }
    }

    public void save(final Set<Object> objects, final Writer out) throws IOException {
        final PrintWriter writer = new PrintWriter(out);
        final SavedObjects saved = new SavedObjects();
        for (final Object object : objects) {
            final ObjectAdapter adapter = getPersistenceSession().adapterFor(object);
            saveData(writer, adapter, saved);
        }
        out.close();
    }

    private void saveData(final PrintWriter writer, final ObjectAdapter adapter, final SavedObjects saved) {
        final String id = saved.getId(adapter);
        writer.println(adapter.getSpecification().getFullIdentifier() + "#" + id);

        final ObjectSpecification adapterSpec = adapter.getSpecification();
        final List<ObjectAssociation> associations = adapterSpec.getAssociations(Contributed.EXCLUDED);
        for (final ObjectAssociation association : associations) {
            if (association.isNotPersisted()) {
                continue;
            }

            final ObjectAdapter associatedObject = association.get(adapter, InteractionInitiatedBy.FRAMEWORK);
            final boolean isEmpty = association.isEmpty(adapter, InteractionInitiatedBy.FRAMEWORK);
            final String associationId = association.getId();
            writer.write("  " + associationId + ": ");
            if (isEmpty) {
                writer.println();
                continue;
            }

            if (association.isOneToManyAssociation()) {
                final CollectionFacet facet = associatedObject.getSpecification().getFacet(CollectionFacet.class);
                for (final ObjectAdapter element : facet.iterable(associatedObject)) {
                    final String refId = saved.getId(element);
                    final String cls = element.getSpecification().getFullIdentifier();
                    writer.print(cls + "#" + refId + " ");
                }
                writer.println();
            } else if (association.getSpecification().isParseable()) {
                final ParseableFacet facet = associatedObject.getSpecification().getFacet(ParseableFacet.class);
                String encodedValue = facet.parseableTitle(associatedObject);
                encodedValue = encodedValue.replaceAll("\n", "\\n");
                writer.println(encodedValue);
            } else if (association.isOneToOneAssociation()) {
                final String refId = saved.getId(associatedObject);
                final String cls = associatedObject.getSpecification().getFullIdentifier();
                writer.println(cls + "#" + refId);
            }
        }
    }


    @javax.inject.Inject
    IsisSessionFactory isisSessionFactory;

    protected PersistenceSession getPersistenceSession() {
        return isisSessionFactory.getCurrentSession().getPersistenceSession();
    }

    @javax.inject.Inject
    SpecificationLoader specificationLoader;

}


class LoadedObjects {
    private final Map<String, ObjectAdapter> idMap = Maps.newHashMap();
    private final Set<Object> objects;
    private final SpecificationLoader specificationLoader;
    private final PersistenceSession persistenceSession;

    public LoadedObjects(
            final Set<Object> objects,
            final SpecificationLoader specificationLoader,
            final PersistenceSession persistenceSession) {
        this.objects = objects;
        this.specificationLoader = specificationLoader;
        this.persistenceSession = persistenceSession;
    }

    public ObjectAdapter get(final String data) {
        final int pos = data.indexOf('#');
        if (pos == -1) {
            throw new FixtureException("load failed - trying to read non-reference data as a reference: " + data);
        }
        final String id = data.substring(pos + 1);
        ObjectAdapter object = idMap.get(id);
        if (object == null) {
            final String className = data.substring(0, pos);
            final ObjectSpecification specification = specificationLoader.loadSpecification(className);
            object = persistenceSession.createTransientInstance(specification);
            idMap.put(id, object);
            objects.add(object.getObject());
        }
        return object;
    }
}

class SavedObjects {
    private int id = 1;
    private final Map<ObjectAdapter, String> idMap = new HashMap<ObjectAdapter, String>();

    public String getId(final ObjectAdapter object) {
        String idString = idMap.get(object);
        if (idString == null) {
            id++;
            idMap.put(object, "" + id);
            idString = "" + id;
        }
        return idString;
    }
}

