/*
 * XAdES4j - A Java library for generation and verification of XAdES signatures.
 * Copyright (C) 2010 Luis Goncalves.
 *
 * XAdES4j is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or any later version.
 *
 * XAdES4j is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with XAdES4j. If not, see <http://www.gnu.org/licenses/>.
 */
package xades4j.utils;

import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.util.Modules;
import com.google.inject.util.Types;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * Helper class that implements the core logic of profile resolution based on a series
 * of dependencies. Profile resultion is based on Google's dependency container
 * (Guice).
 * @see xades4j.production.XadesSigningProfile
 * @see xades4j.verification.XadesVerificationProfile
 * @see xades4j.production.XadesFormatExtenderProfile
 * @author Luís
 */
public final class XadesProfileCore
{
    private static interface BindingAction
    {
        void bind(Binder b);
    }
    /**/
    private final Collection<BindingAction> bindings;

    public XadesProfileCore()
    {
        this.bindings = new ArrayList<BindingAction>();
    }

    /**
     * @throws NullPointerException if {@code from} or {@code to} are null
     */
    public <T> void addBinding(final Class<T> from, final Class<? extends T> to)
    {
        if (null == from || null == to)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                b.bind(from).to(to);
            }
        });
    }

    /**
     * @throws NullPointerException if {@code from} or {@code to} are null
     */
    public <T> void addBinding(final Class<T> from, final T to)
    {
        if (null == from || null == to)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                b.bind(from).toInstance(to);
            }
        });
    }

    public void addGenericBinding(
            final Type genericClass,
            final Class to,
            final Type... genericClassParams)
    {
        if (ObjectUtils.anyNull(genericClass, genericClassParams, to))
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                ParameterizedType pt = Types.newParameterizedType(genericClass, genericClassParams);
                Key k = Key.get(TypeLiteral.get(pt));
                b.bind(k).to(to);
            }
        });
    }

    public void addGenericBinding(
            final Type genericClass,
            final Object to,
            final Type... genericClassParams)
    {
        if (ObjectUtils.anyNull(genericClass, genericClassParams, to))
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                ParameterizedType pt = Types.newParameterizedType(genericClass, genericClassParams);
                Key k = Key.get(TypeLiteral.get(pt));
                b.bind(k).toInstance(to);
            }
        });
    }

    public <T> void addMultibinding(final Class<T> from, final Class<? extends T> to)
    {
        if (null == from || null == to)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                Multibinder multibinder = Multibinder.newSetBinder(b, from);
                multibinder.addBinding().to(to);
            }
        });
    }

    public <T> void addMultibinding(final Class<T> from, final T to)
    {
        if (null == from || null == to)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                Multibinder multibinder = Multibinder.newSetBinder(b, from);
                multibinder.addBinding().toInstance(to);
            }
        });
    }

    public <T, TV extends T> void addMapBinding(final Class<T> valueClass, final Object key, final TV value)
    {
        if (null == key || null == value)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                MapBinder mapBinder = MapBinder.newMapBinder(b, key.getClass(), valueClass);
                mapBinder.addBinding(key).toInstance(value);
            }
        });
    }

    public <T> void addMapBinding(final Class<T> valueClass, final Object key, final Class<? extends T> to)
    {
        if (null == key || null == to)
            throw new NullPointerException();

        this.bindings.add(new BindingAction()
        {
            @Override
            public void bind(Binder b)
            {
                MapBinder mapBinder = MapBinder.newMapBinder(b, key.getClass(), valueClass);
                mapBinder.addBinding(key).to(to);
            }
        });
    }

    public <T> T getInstance(
            Class<T> clazz,
            Module[] overridableModules,
            Module[] sealedModules) throws XadesProfileResolutionException
    {
        Module userBindingsModule = new Module()
        {
            @Override
            public void configure(Binder b)
            {
                for (BindingAction ba : bindings)
                {
                    ba.bind(b);
                }
            }
        };
        Module overridesModule = Modules.override(overridableModules).with(userBindingsModule);
        // Concat sealed modules with overrides module
        Module[] finalModules = Arrays.copyOf(sealedModules, sealedModules.length + 1);
        finalModules[finalModules.length - 1] = overridesModule;
        try
        {
            return Guice.createInjector(finalModules).getInstance(clazz);
        }
        catch (RuntimeException ex)
        {
            throw new XadesProfileResolutionException(ex.getMessage(), ex);
        }
    }
}