/*
 * Copyright 2015 Christian Basler
 *
 * Licensed 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 ch.dissem.bitmessage.factory;

import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.NetworkMessage;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;

/**
 * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams}
 */
public class Factory {
    public static final Logger LOG = LoggerFactory.getLogger(Factory.class);

    public static NetworkMessage getNetworkMessage(int version, InputStream stream) throws SocketTimeoutException {
        try {
            return V3MessageFactory.read(stream);
        } catch (SocketTimeoutException e) {
            throw e;
        } catch (Exception e) {
            LOG.error(e.getMessage(), e);
            return null;
        }
    }

    public static ObjectMessage getObjectMessage(int version, InputStream stream, int length) {
        try {
            return V3MessageFactory.readObject(stream, length);
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
            return null;
        }
    }

    public static Pubkey createPubkey(long version, long stream, byte[] publicSigningKey, byte[] publicEncryptionKey,
                                      long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) {
        return createPubkey(version, stream, publicSigningKey, publicEncryptionKey, nonceTrialsPerByte, extraBytes,
                Pubkey.Feature.bitfield(features));
    }

    public static Pubkey createPubkey(long version, long stream, byte[] publicSigningKey, byte[] publicEncryptionKey,
                                      long nonceTrialsPerByte, long extraBytes, int behaviourBitfield) {
        if (publicSigningKey.length != 64 && publicSigningKey.length != 65)
            throw new IllegalArgumentException("64 bytes signing key expected, but it was "
                    + publicSigningKey.length + " bytes long.");
        if (publicEncryptionKey.length != 64 && publicEncryptionKey.length != 65)
            throw new IllegalArgumentException("64 bytes encryption key expected, but it was "
                    + publicEncryptionKey.length + " bytes long.");

        switch ((int) version) {
            case 2:
                return new V2Pubkey.Builder()
                        .stream(stream)
                        .publicSigningKey(publicSigningKey)
                        .publicEncryptionKey(publicEncryptionKey)
                        .behaviorBitfield(behaviourBitfield)
                        .build();
            case 3:
                return new V3Pubkey.Builder()
                        .stream(stream)
                        .publicSigningKey(publicSigningKey)
                        .publicEncryptionKey(publicEncryptionKey)
                        .behaviorBitfield(behaviourBitfield)
                        .nonceTrialsPerByte(nonceTrialsPerByte)
                        .extraBytes(extraBytes)
                        .build();
            case 4:
                return new V4Pubkey(
                        new V3Pubkey.Builder()
                                .stream(stream)
                                .publicSigningKey(publicSigningKey)
                                .publicEncryptionKey(publicEncryptionKey)
                                .behaviorBitfield(behaviourBitfield)
                                .nonceTrialsPerByte(nonceTrialsPerByte)
                                .extraBytes(extraBytes)
                                .build()
                );
            default:
                throw new IllegalArgumentException("Unexpected pubkey version " + version);
        }
    }

    public static BitmessageAddress generatePrivateAddress(boolean shorter, long stream, Pubkey.Feature... features) {
        return new BitmessageAddress(new PrivateKey(shorter, stream, 1000, 1000, features));
    }

    static ObjectPayload getObjectPayload(long objectType, long version, long streamNumber, InputStream stream, int length) throws IOException {
        ObjectType type = ObjectType.fromNumber(objectType);
        if (type != null) {
            switch (type) {
                case GET_PUBKEY:
                    return parseGetPubkey(version, streamNumber, stream, length);
                case PUBKEY:
                    return parsePubkey(version, streamNumber, stream, length);
                case MSG:
                    return parseMsg(version, streamNumber, stream, length);
                case BROADCAST:
                    return parseBroadcast(version, streamNumber, stream, length);
                default:
                    LOG.error("This should not happen, someone broke something in the code!");
            }
        }
        // fallback: just store the message - we don't really care what it is
//        LOG.info("Unexpected object type: " + objectType);
        return GenericPayload.read(stream, streamNumber, length);
    }

    private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
        return GetPubkey.read(stream, streamNumber, length, version);
    }

    public static Pubkey readPubkey(long version, long stream, InputStream is, int length, boolean encrypted) throws IOException {
        switch ((int) version) {
            case 2:
                return V2Pubkey.read(is, stream);
            case 3:
                return V3Pubkey.read(is, stream);
            case 4:
                return V4Pubkey.read(is, stream, length, encrypted);
        }
        LOG.debug("Unexpected pubkey version " + version + ", handling as generic payload object");
        return null;
    }

    private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException {
        Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true);
        return pubkey != null ? pubkey : GenericPayload.read(stream, streamNumber, length);
    }

    private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException {
        return Msg.read(stream, streamNumber, length);
    }

    private static ObjectPayload parseBroadcast(long version, long streamNumber, InputStream stream, int length) throws IOException {
        switch ((int) version) {
            case 4:
                return V4Broadcast.read(stream, streamNumber, length);
            case 5:
                return V5Broadcast.read(stream, streamNumber, length);
            default:
                LOG.debug("Encountered unknown broadcast version " + version);
                return GenericPayload.read(stream, streamNumber, length);
        }
    }
}
