/**
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 *
 * Copyright (C)
 * <a href="http://www.inab.org">Spanish National Institute of Bioinformatics (INB)</a>
 * <a href="http://www.bsc.es">Barcelona Supercomputing Center (BSC)</a>
 * <a href="http://inb.bsc.es">Computational Node 6</a>
 */

package org.inb.biomoby.central.cache;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.transform.stream.StreamSource;
import org.inb.biomoby.central.MobyCentral;
import org.inb.biomoby.shared.registry.*;

/**
 * BioMoby Registry cache implementation.
 * By default a cache location is in a $HOME/.MobyCentralCache/ directory, but
 * may be overriden by providing a custom implementation of IMobyCentralCache interface.
 *
 * @author Dmitry Repchevsky
 */

public class CacheConfig
{    
    private static CacheConfig config;

    private IMobyCentralCache cache;
    private boolean isCached;
    
    private CacheConfig()
    {
        isCached = true;
    }

    /**
     * Method to check whether a cache must be used
     *
     * @return true if cache is enabled, false otherwise.
     */
    public boolean isCached()
    {
        return isCached;
    }

    /**
     * Method to mark that a cache must be used.
     *
     * @param isCached - true to enable a cache, false otherwise
     */
    public void setCached(boolean isCached)
    {
        this.isCached = isCached;
    }

    /**
     * Method allows override a default cache location.
     *
     * @param cache - a custom cache implementation
     */
    public synchronized void setCache(IMobyCentralCache cache)
    {
        this.cache = cache;
    }

    /**
     * Method returns a current cache locator (default is MobyCentralFileCache.java).
     *
     * @return - a cache locator object
     */
    private synchronized IMobyCentralCache getCache()
    {
        if (cache == null)
        {
            cache = new MobyCentralFileCache();
        }

        return cache;
    }

    private synchronized Reader getReader(String fileName) throws Exception
    {
        URL url = new URL("file", "localhost", fileName);

        IMobyCentralCache c = getCache();
        Reader reader = c.getReader(url);
        if (reader == null && !(c instanceof MobyCentralFileCache))
        {
            reader = new MobyCentralFileCache().getReader(url);
        }

        return reader;
    }

    private synchronized Writer getWriter(String fileName) throws Exception
    {
        URL url = new URL("file", "localhost", fileName);

        IMobyCentralCache c = getCache();
        Writer writer = c.getWriter(url);
        if (writer == null && !(c instanceof MobyCentralFileCache))
        {
            writer = new MobyCentralFileCache().getWriter(url);
        }

        return writer;
    }

    /**
     * Generic method that saves any BioMoby entities ("Namespace", "Service", "ServiceType" and "ObjectType")
     * to a cache file.
     *
     * @param <T> - the entity type to save
     * @param entities - the list of entities to be saved
     * @param central - a BioMoby registry the cache belongs to
     * @param file - a file name entities to be saved (for instance "MobyServiceList" ...)
     * @throws Exception
     */
    public <T extends AbstractEntity> void save(List<T> entities, MobyCentral central, String file) throws Exception
    {
        String fileName = getFileName(central, file);
        Writer writer = getWriter(fileName);

        if (writer == null)
        {
            throw new IOException("cant write cache file " + fileName);
        }

        try
        {
            save(entities, writer);
        }
        finally
        {
            writer.close();
        }
    }

    /**
     * Generic method to save BioMoby entities to the provided writer
     *
     * @param <T> - the entity type to save
     * @param entities - the list of entities to be saved
     * @param writer - the writer where entities are saved
     * @throws Exception
     */
    public <T extends AbstractEntity> void save(List<T> entities, Writer writer) throws Exception
    {
        CacheListWrapper wrapper = new CacheListWrapper(entities);

        JAXBContext jc = JAXBContext.newInstance(CacheListWrapper.class, MobyCentral.class, AbstractEntity.class, AbstractTypeEntity.class,
                Collection.class, Data.class, Input.class, Namespace.class, ObjectType.class, Output.class,
                Parameter.class, Registration.class, Relationship.class, Resource.class, Service.class,
                ServiceProvider.class, ServiceType.class, Simple.class);

        Marshaller m = jc.createMarshaller();

        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

        BufferedWriter out = new BufferedWriter(writer);

        try
        {
            m.marshal(wrapper, out);
        }
        finally
        {
            out.flush();
        }
    }

    /**
     * Generic method to load BioMoby entities from a cache.
     *
     * @param <T> - the entity type to be load from the cache
     * @param central - a BioMoby registry the cache belongs to
     * @param file - file name from which to load entities
     * @return - a list of loaded entities
     * @throws Exception
     */
    public <T extends AbstractEntity> List<T> load(MobyCentral central, String file) throws Exception
    {
        String fileName = getFileName(central, file);
        Reader reader = getReader(fileName);

        if (reader == null)
        {
            return null;
        }

        try
        {
            return load(reader);
        }
        finally
        {
            reader.close();
        }
    }

    /**
     * Generic method to load BioMoby entities from a cache.
     *
     * @param <T> - the entity type to be load from the cache
     * @param reader - a reader to load entities from.
     * @return - a list of loaded entities
     * @throws Exception
     */
    public <T extends AbstractEntity> List<T> load(Reader reader) throws Exception
    {
        try
        {
            JAXBContext jc = JAXBContext.newInstance(CacheListWrapper.class, MobyCentral.class, AbstractEntity.class, AbstractTypeEntity.class,
                    Collection.class, Data.class, Input.class, Namespace.class, ObjectType.class, Output.class,
                    Parameter.class, Registration.class, Relationship.class, Resource.class, Service.class,
                    ServiceProvider.class, ServiceType.class, Simple.class);

            Unmarshaller um = jc.createUnmarshaller();

            BufferedReader in = new BufferedReader(reader);

            CacheListWrapper wrapper =  (CacheListWrapper)um.unmarshal(new StreamSource(in));

            return wrapper.getEntities();
        }
        finally
        {
            reader.close();
        }
    }

    private String getFileName(MobyCentral central, String file)
    {
        String url = central.getEndpointURL();
        
        int h = 0;
        for (int i = 0, n = url.length(); i < n; i++)
        {
            h = 31*h + url.charAt(i);
        }
        
        return file + '-' + String.valueOf(Math.abs(h)) + ".xml";
    }

    /**
     * A singleton instance creator. This is the only way to construct a CacheConfig object.
     * The may be only one CacheConfig per application.
     *
     * @return a singleton CacheConfig object.
     */
    public static synchronized CacheConfig instance()
    {
        if (config == null)
        {
            config = new CacheConfig();
        }
        
        return config;
    }

    /**
     * The auxiliary class for a cache serialization
     */
    @XmlRootElement(name="cache")
    static class CacheListWrapper
    {
        private List entities;

        CacheListWrapper() {}

        CacheListWrapper(List entities)
        {
            this.entities = entities;
        }

        @XmlElementRefs({
          @XmlElementRef(type=Service.class),
          @XmlElementRef(type=ServiceType.class),
          @XmlElementRef(type=ObjectType.class),
          @XmlElementRef(type=Namespace.class),
          @XmlElementRef(type=MobyCentral.class)})
        <T extends AbstractEntity> List<T> getEntities()
        {
            if (entities == null)
            {
                entities = new ArrayList();
            }

            return entities;
        }
    }
}
