/*
 * JBoss, Home of Professional Open Source
 * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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 org.aesh.terminal.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.aesh.terminal.io.Decoder;
import org.aesh.terminal.io.Encoder;
import org.aesh.terminal.Attributes;
import org.aesh.terminal.Connection;
import org.aesh.terminal.Device;
import org.aesh.terminal.EventDecoder;
import org.aesh.terminal.tty.Capability;
import org.aesh.terminal.tty.Signal;
import org.aesh.terminal.tty.Size;
import org.aesh.terminal.tty.TtyOutputMode;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.function.Consumer;

/**
 * A connection to an http client, independent of the protocol, it could be straight WebSockets or
 * SockJS, etc...
 *
 * The incoming protocol is based on json messages:
 *
 * {
 *   "action": "read",
 *   "data": "what the user typed"
 * }
 *
 * or
 *
 * {
 *   "action": "resize",
 *   "cols": 30,
 *   "rows: 50
 * }
 *
 * @author <a href="mailto:[email protected]">Julien Viet</a>
 * @author <a href="mailto:[email protected]">Matej Lazar</a>
 */
public abstract class HttpTtyConnection implements Connection {

    public static final Size DEFAULT_SIZE = new Size(80, 24);
    private final Device device;

    private Charset charset;
    private Size size;
    private Consumer<Size> sizeHandler;
    private final EventDecoder eventDecoder;
    private final Decoder decoder;
    private final Consumer<int[]> stdout;
    private Consumer<Void> closeHandler;
    private Consumer<String> termHandler;
    private long lastAccessedTime = System.currentTimeMillis();
    private Attributes attributes;

    public HttpTtyConnection() {
        this(StandardCharsets.UTF_8, DEFAULT_SIZE);
    }

    public HttpTtyConnection(Charset charset, Size size) {
        this.charset = charset;
        this.size = size;
        this.eventDecoder = new EventDecoder(3, 4, 26);
        this.decoder = new Decoder(512, charset, eventDecoder);
        this.stdout = new TtyOutputMode(new Encoder(charset, this::write));

        this.device = new HttpDevice("vt100");
        attributes = new Attributes();
    }

    @Override
    public Charset outputEncoding() {
        return charset;
    }

    @Override
    public Charset inputEncoding() {
        return charset;
    }

    public long lastAccessedTime() {
        return lastAccessedTime;
    }

    @Override
    public Device device() {
        return device;
    }

    protected abstract void write(byte[] buffer);

    @SuppressWarnings("unchecked")
    public void writeToDecoder(String msg) {
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> obj;
        String action;
        try {
            obj = mapper.readValue(msg, Map.class);
            action = (String) obj.get("action");
        } catch (IOException e) {
            // Log this
            return;
        }
        if (action != null) {
            switch (action) {
                case "read":
                    lastAccessedTime = System.currentTimeMillis();
                    String data = (String) obj.get("data");
                    decoder.write(data.getBytes()); //write back echo
                    break;
                case "resize":
                    try {
                        int cols = (int) obj.getOrDefault("cols", size.getWidth());
                        int rows = (int) obj.getOrDefault("rows", size.getHeight());
                        if (cols > 0 && rows > 0) {
                            Size newSize = new Size(cols, rows);
                            if (!newSize.equals(size())) {
                                size = newSize;
                                if (sizeHandler != null) {
                                    sizeHandler.accept(size);
                                }
                            }
                        }
                    } catch (Exception e) {
                        // Invalid size
                        // Log this
                    }
                    break;
            }
        }
    }

    public Consumer<String> getTerminalTypeHandler() {
        return termHandler;
    }

    public void setTerminalTypeHandler(Consumer<String> handler) {
        termHandler = handler;
    }

    @Override
    public Size size() {
        return size;
    }

    public Consumer<Size> getSizeHandler() {
        return sizeHandler;
    }

    public void setSizeHandler(Consumer<Size> handler) {
        this.sizeHandler = handler;
    }

    @Override
    public Consumer<Signal> getSignalHandler() {
        return eventDecoder.getSignalHandler();
    }

    @Override
    public void setSignalHandler(Consumer<Signal> handler) {
        eventDecoder.setSignalHandler(handler);
    }

    public Consumer<int[]> getStdinHandler() {
        return eventDecoder.getInputHandler();
    }

    public void setStdinHandler(Consumer<int[]> handler) {
        eventDecoder.setInputHandler(handler);
    }

    public Consumer<int[]> stdoutHandler() {
        return stdout;
    }

    @Override
    public void setCloseHandler(Consumer<Void> closeHandler) {
        this.closeHandler = closeHandler;
    }

    @Override
    public Consumer<Void> getCloseHandler() {
        return closeHandler;
    }

    @Override
    public void openBlocking() {
    }

    @Override
    public void openNonBlocking() {
    }

    @Override
    public boolean put(Capability capability, Object... params) {
        return false;
    }

    @Override
    public Attributes getAttributes() {
        return attributes;
    }

    @Override
    public void setAttributes(Attributes attr) {
    }

    @Override
    public boolean supportsAnsi() {
        return true;
    }
}