/*
 This file is part of GNUnet.
  (C) 2012, 2013 Christian Grothoff (and other contributing authors)

  GNUnet is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published
  by the Free Software Foundation; either version 3, or (at your
  option) any later version.

  GNUnet 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
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with GNUnet; see the file COPYING.  If not, write to the
  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  Boston, MA 02111-1307, USA.
 */

package org.gnunet.identity;


import com.google.common.collect.Lists;
import org.gnunet.identity.messages.*;
import org.gnunet.mq.Envelope;
import org.gnunet.requests.Request;
import org.gnunet.requests.RequestIdentifier;
import org.gnunet.requests.SequentialRequestContainer;
import org.gnunet.util.*;
import org.gnunet.util.crypto.EcdsaPrivateKey;
import org.gnunet.util.crypto.EcdsaPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Make requests to the identity service.
 */
public class Identity {
    private static final Logger logger = LoggerFactory
            .getLogger(Identity.class);
    private Configuration configuration;
    private SequentialRequestContainer<IdentityRequest> requests;
    private Client client;
    private IdentityListCallback identityListCallback;
    private List<Ego> knownEgos = Lists.newLinkedList();

    private static Ego anonymousEgo;

    /**
     * Look up the given ego name.
     *
     * @param configuration configuration to use for connecting to
     *                      the identity service
     * @param egoName ego name to look up
     * @param identityCallback handler for lookup result
     * @return a handle to cancel the lookup
     */
    public static Cancelable lookup(Configuration configuration,
                                    final String egoName, final IdentityCallback identityCallback) {
        final Identity myIdentity = new Identity();
        final CancellationToken cancellationToken = new CancellationToken();
        myIdentity.connect(configuration, new IdentityListCallback() {
            boolean done = false;
            @Override
            public void onEgoAdd(Ego ego) {
                if (done) {
                    throw new AssertionError();
                }
                if (ego.name.equals(egoName)) {
                    done = true;
                    cancellationToken.cancel();
                    identityCallback.onEgo(ego);
                }
            }

            @Override
            public void onEgoDelete(Ego ego) {
                if (done) {
                    throw new AssertionError();
                }
                logger.error("unsolicited ego delete on lookup");
                done = true;
                cancellationToken.cancel();
                identityCallback.onError("identity service misbehaved");
            }

            @Override
            public void onEgoRename(String oldName, Ego ego) {
                if (done) {
                    throw new AssertionError();
                }
                logger.error("unsolicited ego rename on lookup");
                done = true;
                cancellationToken.cancel();
                identityCallback.onError("identity service misbehaved");
            }

            @Override
            public void onListEnd() {
                if (done) {
                    throw new AssertionError();
                }
                done = true;
                cancellationToken.cancel();
                identityCallback.onError("identity not found");

            }
        });
        cancellationToken.add(new Cancelable() {
            @Override
            public void cancel() {
                myIdentity.disconnect();
            }
        });
        return cancellationToken;
    }

    /**
     * An ego.
     */
    public static class Ego {
        private String name;
        private EcdsaPrivateKey privateKey;

        /**
         * Create an ego with the given name and private key.
         *
         * @param egoName the ego name
         * @param privateKey the ego's private key
         */
        public Ego(String egoName, EcdsaPrivateKey privateKey) {
            this.name = egoName;
            this.privateKey = privateKey;
        }

        /**
         * Get the ego's private key.
         *
         * @return the ego's private key
         */
        public EcdsaPrivateKey getPrivateKey() {
            return privateKey;
        }

        /**
         * Compute the ego's public key from its private key.
         *
         * @return the ego's public key
         */
        public EcdsaPublicKey getPublicKey() {
            return privateKey.getPublicKey();
        }

        /**
         * Get the ego's name.
         *
         * @return the ego's name
         */
        public String getName() {
            return name;
        }
    }

    public abstract class IdentityRequest extends Request {

        public void onError(String errorMessage) {
            throw new AssertionError("unexpected error message: " + errorMessage);
        }

        public void onResult() {
            throw new AssertionError("unexpected result");
        }
    }

    public class GetDefaultRequest extends IdentityRequest {
        IdentityCallback identityCallback;
        String serviceName;
        GetDefaultRequest(String serviceName, IdentityCallback identityCallback) {
            this.identityCallback = identityCallback;
            this.serviceName = serviceName;
        }
        @Override
        public Envelope assembleRequest() {
            GetDefaultMessage m = new GetDefaultMessage();
            m.nameLength = serviceName.length() + 1;
            m.serviceName = serviceName;
            return new Envelope(m);
        }

        @Override
        public void onError(String errorMessage) {
            identityCallback.onError(errorMessage);
        }
    }

    public class SetDefaultRequest extends IdentityRequest {
        IdentityContinuation cont;
        String serviceName;
        Ego ego;
        SetDefaultRequest(String serviceName, Ego ego) {
            this.serviceName = serviceName;
            this.ego = ego;
        }
        @Override
        public Envelope assembleRequest() {
            SetDefaultMessage m = new SetDefaultMessage();
            m.nameLength = serviceName.length() + 1;
            m.serviceName = serviceName;
            m.privateKey = ego.privateKey;
            return new Envelope(m);
        }
        @Override
        public void onError(String errorMessage) {
            cont.onError(errorMessage);
        }
        @Override
        public void onResult() {
            cont.onDone();
        }
    }

    public class RenameRequest extends IdentityRequest {
        IdentityContinuation cont;
        String oldName;
        String newName;
        RenameRequest(String newName, String oldName) {
            this.oldName = oldName;
            this.newName = newName;
        }
        @Override
        public Envelope assembleRequest() {
            RenameMessage m = new RenameMessage();
            m.newName = newName;
            m.newNameLength = newName.length() + 1;
            m.oldName = oldName;
            m.oldNameLength = oldName.length() + 1;
            return new Envelope(m);
        }
        @Override
        public void onError(String errorMessage) {
            cont.onError(errorMessage);
        }
        @Override
        public void onResult() {
            cont.onDone();
        }
    }

    public class DeleteRequest extends IdentityRequest {
        IdentityContinuation cont;
        String name;
        DeleteRequest(String name) {
            this.name = name;
        }
        @Override
        public Envelope assembleRequest() {
            DeleteMessage m = new DeleteMessage();
            m.name = name;
            m.nameLength = name.length() + 1;
            return new Envelope(m);
        }
        @Override
        public void onError(String errorMessage) {
            cont.onError(errorMessage);
        }
        @Override
        public void onResult() {
            cont.onDone();
        }
    }

    public class CreateRequest extends IdentityRequest {
        final EcdsaPrivateKey privateKey;
        IdentityContinuation cont;
        String name;
        CreateRequest(String name, EcdsaPrivateKey privateKey, IdentityContinuation cont) {
            this.cont = cont;
            this.privateKey = privateKey;
            this.name = name;
        }
        @Override
        public Envelope assembleRequest() {
            CreateRequestMessage m = new CreateRequestMessage();
            m.name = name;
            m.nameLength = name.length() + 1;
            m.privateKey = privateKey;
            return new Envelope(m);
        }
        @Override
        public void onError(String errorMessage) {
            cont.onError(errorMessage);
        }

        @Override
        public void onResult() {
            cont.onDone();
        }
    }

    /**
     * Get the anonymous ego.  The anonymous ego has a publicly
     * known private key.
     *
     * @return the anonymous ego
     */
    public static Ego getAnonymousEgo() {
        if (anonymousEgo == null) {
            anonymousEgo = new Ego(null, EcdsaPrivateKey.getAnonymous());
        }
        return anonymousEgo;
    }


    /**
     * Create a handle that can connect to the identity service.
     * Nothing will happen until calling connect.
     */
    public Identity() {
        // do nothing
    }

    /**
     * Connect to the identity service.
     *
     * @param configuration configuration to use
     * @param identityListCallback callback that receives initially the list of all egos,
     *                             and subsequently changes to egos
     */
    public void connect(Configuration configuration, IdentityListCallback identityListCallback) {
        this.configuration = configuration;
        this.client = new Client("identity", configuration);
        this.client.installReceiver(new IdentityReceiver());
        requests = new SequentialRequestContainer<IdentityRequest>(this.client);
        this.identityListCallback = identityListCallback;
        StartMessage m = new StartMessage();
        client.send(m);
    }

    /**
     * Get the default ego for a service
     *
     * @param serviceName name of the service
     * @param identityCallback callback that receives the default ego
     * @return a handle to cancel the operation
     */
    public Cancelable get(String serviceName, IdentityCallback identityCallback) {
        return requests.addRequest(new GetDefaultRequest(serviceName, identityCallback));
    }

    /**
     * Set the default ego for a service.
     *
     * @param serviceName service
     * @param ego ego
     * @return object for cancellation
     */
    public Cancelable set(String serviceName, Ego ego) {
        return requests.addRequest(new SetDefaultRequest(serviceName, ego));
    }

    /**
     * Disconnect from the identity service.
     */
    public void disconnect() {
        client.disconnect();
        client = null;
    }

    /**
     * Create a new ego with the given name.
     *
     * @param name ego name
     * @param cont continuation
     * @return object for cancellation
     */
    public Cancelable create(String name, IdentityContinuation cont) {
        EcdsaPrivateKey privateKey = EcdsaPrivateKey.createRandom();
        return requests.addRequest(new CreateRequest(name, privateKey, cont));
    }

    /**
     * Rename an ego.
     *
     * @param oldName old name of the ego
     * @param newName new name of the ego
     * @return object for cancellation
     */
    public Cancelable rename(String oldName, String newName) {
        return requests.addRequest(new RenameRequest(newName, oldName));
    }

    /**
     * Delete an ego.
     *
     * @param name name of the ego to delete
     * @return object for cancellation
     */
    public Cancelable delete(String name) {
        return requests.addRequest(new DeleteRequest(name));
    }

    private Ego getEgoForKey(EcdsaPrivateKey privateKey) {
        for (Ego ex : knownEgos) {
            if (ex.privateKey.equals(privateKey)) {
                return ex;
            }
        }
        return null;
    }

    public class IdentityReceiver extends RunaboutMessageReceiver {
        @Override
        public void handleError() {
            logger.warn("identity service disconnected");
            // FIXME: should have exp. backoff
            client.reconnect();
        }

        public void visit(ResultCodeMessage m) {
            RequestIdentifier<IdentityRequest> rId = requests.getRequestIdentifier();
            IdentityRequest r = rId.getRequest();
            if (null == r) {
                logger.warn("unsolicited result code message");
                return;
            }
            if (m.errorMessage != null) {
                r.onError(m.errorMessage);
            } else {
                r.onResult();
            }
            rId.retire();
        }

        public void visit(SetDefaultMessage m) {
            RequestIdentifier<IdentityRequest> rId = requests.getRequestIdentifier();
            IdentityRequest r = rId.getRequest();
            if (!(r instanceof GetDefaultRequest)) {
                logger.error("unexpected 'default ego' response");
                return;
            }
            GetDefaultRequest gdr = (GetDefaultRequest) r;
            Ego ego = getEgoForKey(m.privateKey);
            if (null != ego)
                gdr.identityCallback.onEgo(ego);
            rId.retire();
        }

        public void visit(final UpdateListMessage m) {
            if (m.endOfList != 0) {
                if (null != identityListCallback)
                    identityListCallback.onListEnd();
                return;
            }
            if (m.nameLength == 0) {
                Ego e = getEgoForKey(m.privateKey);
                if (null != e) {
                    knownEgos.remove(e);
                }
                if (null != identityListCallback)
                    identityListCallback.onEgoDelete(e);
            } else {
                Ego existingEgo = getEgoForKey(m.privateKey);
                if (existingEgo == null) {
                    Ego ego = new Ego(m.egoName, m.privateKey);
                    knownEgos.add(ego);
                    if (null != identityListCallback)
                        identityListCallback.onEgoAdd(ego);
                } else {
                    // rename
                    String oldName = existingEgo.name;
                    existingEgo.name = m.egoName;
                    if (null != identityListCallback)
                        identityListCallback.onEgoRename(oldName, existingEgo);
                }
            }
        }
    }
}
