using Gee;
using Xmpp.Xep.Jingle;

namespace Xmpp.Xep.Jet {
public const string NS_URI = "urn:xmpp:jingle:jet:0";

public class Module : XmppStreamModule, SecurityPrecondition {
    public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0391_jet");
    private HashMap<string, EnvelopEncoding> envelop_encodings = new HashMap<string, EnvelopEncoding>();
    private HashMap<string, Cipher> ciphers = new HashMap<string, Cipher>();

    public override void attach(XmppStream stream) {
        stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
        stream.get_module(Jingle.Module.IDENTITY).register_security_precondition(this);
    }

    public override void detach(XmppStream stream) {
        stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
    }

    public async bool is_available(XmppStream stream, Jid full_jid) {
        return yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI);
    }

    public void register_envelop_encoding(EnvelopEncoding encoding) {
        envelop_encodings[encoding.get_type_uri()] = encoding;
    }

    public void register_cipher(Cipher cipher) {
        ciphers[cipher.get_cipher_uri()] = cipher;
    }

    public string security_ns_uri() {
        return NS_URI;
    }

    public Jingle.SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error requires (options is Options) {
        Options jet_options = (Options) options;
        string cipher = jet_options.cipher_uri;
        string type = jet_options.type_uri;
        if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) {
            throw new Jingle.Error.UNSUPPORTED_SECURITY("JET cipher or type unknown");
        }
        EnvelopEncoding encoding = envelop_encodings[type];
        return new SecurityParameters(ciphers[cipher], encoding, ciphers[cipher].generate_random_secret(), jet_options);
    }

    public Jingle.SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError {
        string? cipher = security.get_attribute("cipher");
        string? type = security.get_attribute("type");
        if (cipher == null || type == null) {
            throw new IqError.BAD_REQUEST("No cipher or type specified for JET");
        }
        if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) {
            throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown");
        }
        EnvelopEncoding encoding = envelop_encodings[type];
        TransportSecret secret = encoding.decode_envolop(stream, local_full_jid, peer_full_jid, security);
        return new SecurityParameters(ciphers[cipher], encoding, secret);
    }

    public override string get_ns() { return NS_URI; }
    public override string get_id() { return IDENTITY.id; }
}

public class Options : Object {
    public string type_uri { get; private set; }
    public string cipher_uri { get; private set; }

    public Options(string type_uri, string cipher_uri) {
        this.type_uri = type_uri;
        this.cipher_uri = cipher_uri;
    }
}

public class SecurityParameters : Jingle.SecurityParameters, Object {
    public Cipher cipher { get; private set; }
    public EnvelopEncoding encoding { get; private set; }
    public TransportSecret secret { get; private set; }
    public Options? options { get; private set; }

    public SecurityParameters(Cipher cipher, EnvelopEncoding encoding, TransportSecret secret, Options? options = null) {
        this.cipher = cipher;
        this.encoding = encoding;
        this.secret = secret;
        this.options = options;
    }

    public string security_ns_uri() {
        return NS_URI;
    }
    public IOStream wrap_stream(IOStream stream) {
        debug("Wrapping stream into encrypted stream for %s/%s", encoding.get_type_uri(), cipher.get_cipher_uri());
        return new EncryptedStream(cipher, secret, stream);
    }
    public StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) {
        StanzaNode security = new StanzaNode.build("security", NS_URI)
                .add_self_xmlns()
                .put_attribute("cipher", cipher.get_cipher_uri())
                .put_attribute("type", encoding.get_type_uri());
        encoding.encode_envelop(stream, local_full_jid, peer_full_jid, this, security);
        return security;
    }
}

public interface Cipher : Object {
    public abstract string get_cipher_uri();
    public abstract TransportSecret generate_random_secret();
    public abstract InputStream wrap_input_stream(InputStream input, TransportSecret secret);
    public abstract OutputStream wrap_output_stream(OutputStream output, TransportSecret secret);
}

private class EncryptedStream : IOStream {
    private IOStream stream;
    private InputStream input;
    private OutputStream output;
    public override InputStream input_stream { get { return input; } }
    public override OutputStream output_stream { get { return output; } }

    public EncryptedStream(Cipher cipher, TransportSecret secret, IOStream stream) {
        this.stream = stream;
        input = cipher.wrap_input_stream(stream.input_stream, secret);
        output = cipher.wrap_output_stream(stream.output_stream, secret);
    }
}

public class TransportSecret {
    public uint8[] transport_key { get; private set; }
    public uint8[] initialization_vector { get; private set; }
    public TransportSecret(uint8[] transport_key, uint8[] initialization_vector) {
        this.transport_key = transport_key;
        this.initialization_vector = initialization_vector;
    }
}

public interface EnvelopEncoding : Object {
    public abstract string get_type_uri();
    public abstract TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError;
    public abstract void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, SecurityParameters security_params, StanzaNode security);
}

}