/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.samsungtv.internal.protocol;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.net.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link RemoteControllerLegacy} is responsible for sending key codes to the
 * Samsung TV.
 *
 * @see <a
 *      href="http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/">http://sc0ty.pl/2012/02/samsung-tv-
 *      network-remote-control-protocol/</a>
 *
 *
 * @author Pauli Anttila - Initial contribution
 * @author Arjan Mels - Renamed and reworked to use RemoteController base class, to allow different protocols
 */
@NonNullByDefault
public class RemoteControllerLegacy extends RemoteController {

    private static final int CONNECTION_TIMEOUT = 500;

    private final Logger logger = LoggerFactory.getLogger(RemoteControllerLegacy.class);

    // Access granted response
    private static final char[] ACCESS_GRANTED_RESP = new char[] { 0x64, 0x00, 0x01, 0x00 };

    // User rejected your network remote controller response
    private static final char[] ACCESS_DENIED_RESP = new char[] { 0x64, 0x00, 0x00, 0x00 };

    // waiting for user to grant or deny access response
    private static final char[] WAITING_USER_GRANT_RESP = new char[] { 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00 };

    // timeout or cancelled by user response
    private static final char[] ACCESS_TIMEOUT_RESP = new char[] { 0x65, 0x00 };

    private static final String APP_STRING = "iphone.iapp.samsung";

    private @Nullable Socket socket;
    private @Nullable InputStreamReader reader;
    private @Nullable BufferedWriter writer;

    /**
     * Create and initialize remote controller instance.
     *
     * @param host Host name of the Samsung TV.
     * @param port TCP port of the remote controller protocol.
     * @param appName Application name used to send key codes.
     * @param uniqueId Unique Id used to send key codes.
     */
    public RemoteControllerLegacy(String host, int port, @Nullable String appName, @Nullable String uniqueId) {
        super(host, port, appName, uniqueId);
    }

    /**
     * Open Connection to Samsung TV.
     *
     * @throws RemoteControllerException
     */
    @Override
    public void openConnection() throws RemoteControllerException {
        logger.debug("Open connection to host '{}:{}'", host, port);

        Socket localsocket = new Socket();
        socket = localsocket;
        try {
            socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
        } catch (IOException e) {
            logger.debug("Cannot connect to Legacy Remote Controller: {}", e.getMessage());
            throw new RemoteControllerException("Connection failed", e);
        }

        InputStream inputStream;
        try {
            BufferedWriter localwriter = new BufferedWriter(new OutputStreamWriter(localsocket.getOutputStream()));
            writer = localwriter;
            inputStream = localsocket.getInputStream();
            InputStreamReader localreader = new InputStreamReader(inputStream);
            reader = localreader;

            logger.debug("Connection successfully opened...querying access");
            writeInitialInfo(localwriter, localsocket);
            readInitialInfo(localreader);

            int i;
            while ((i = inputStream.available()) > 0) {
                inputStream.skip(i);
            }
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
        try {
            /* @formatter:off
            *
            * offset value and description
            * ------ ---------------------
            * 0x00   0x00 - datagram type?
            * 0x01   0x0013 - string length (little endian)
            * 0x03   "iphone.iapp.samsung" - string content
            * 0x16   0x0038 - payload size (little endian)
            * 0x18   payload
            *
            * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
            * encoded with base64 algorithm. Every string is preceded by
            * 2-bytes field containing encoded string length.
            *
            * These three strings are as follow:
            *
            * remote control device IP, unique ID – value to distinguish
            * controllers, name – it will be displayed as controller name.
            *
            * @formatter:on
            */

            writer.append((char) 0x00);
            writeString(writer, APP_STRING);
            writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
            writer.flush();
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    private void readInitialInfo(Reader reader) throws RemoteControllerException {
        try {
            /* @formatter:off
            *
            * offset value and description
            * ------ ---------------------
            * 0x00   don't know, it it always 0x00 or 0x02
            * 0x01   0x000c - string length (little endian)
            * 0x03   "iapp.samsung" - string content
            * 0x0f   0x0006 - payload size (little endian)
            * 0x11   payload
            *
            * @formatter:on
            */

            reader.skip(1);
            readString(reader);
            char[] result = readCharArray(reader);

            if (Arrays.equals(result, ACCESS_GRANTED_RESP)) {
                logger.debug("Access granted");
            } else if (Arrays.equals(result, ACCESS_DENIED_RESP)) {
                throw new RemoteControllerException("Access denied");
            } else if (Arrays.equals(result, ACCESS_TIMEOUT_RESP)) {
                throw new RemoteControllerException("Registration timed out");
            } else if (Arrays.equals(result, WAITING_USER_GRANT_RESP)) {
                throw new RemoteControllerException("Waiting for user to grant access");
            } else {
                throw new RemoteControllerException("Unknown response received for access query");
            }
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    /**
     * Close connection to Samsung TV.
     *
     * @throws RemoteControllerException
     */
    public void closeConnection() throws RemoteControllerException {
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    /**
     * Send key code to Samsung TV.
     *
     * @param key Key code to send.
     * @throws RemoteControllerException
     */
    @Override
    public void sendKey(KeyCode key) throws RemoteControllerException {
        logger.debug("Try to send command: {}", key);

        if (!isConnected()) {
            openConnection();
        }

        try {
            sendKeyData(key);
        } catch (RemoteControllerException e) {
            logger.debug("Couldn't send command", e);
            logger.debug("Retry one time...");

            closeConnection();
            openConnection();

            sendKeyData(key);
        }

        logger.debug("Command successfully sent");
    }

    /**
     * Send sequence of key codes to Samsung TV.
     *
     * @param keys List of key codes to send.
     * @throws RemoteControllerException
     */
    @Override
    public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
        sendKeys(keys, 300);
    }

    /**
     * Send sequence of key codes to Samsung TV.
     *
     * @param keys List of key codes to send.
     * @param sleepInMs Sleep between key code sending in milliseconds.
     * @throws RemoteControllerException
     */
    public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
        logger.debug("Try to send sequence of commands: {}", keys);

        if (!isConnected()) {
            openConnection();
        }

        for (int i = 0; i < keys.size(); i++) {
            KeyCode key = keys.get(i);
            try {
                sendKeyData(key);
            } catch (RemoteControllerException e) {
                logger.debug("Couldn't send command", e);
                logger.debug("Retry one time...");

                closeConnection();
                openConnection();

                sendKeyData(key);
            }

            if ((keys.size() - 1) != i) {
                // Sleep a while between commands
                try {
                    Thread.sleep(sleepInMs);
                } catch (InterruptedException e) {
                    return;
                }
            }
        }

        logger.debug("Command(s) successfully sent");
    }

    @Override
    public boolean isConnected() {
        return socket != null && !socket.isClosed() && socket != null && socket.isConnected();
    }

    private String createRegistrationPayload(String ip) throws IOException {
        /*
         * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
         * encoded with base64 algorithm. Every string is preceded by 2-bytes
         * field containing encoded string length.
         *
         * These three strings are as follow:
         *
         * remote control device IP, unique ID – value to distinguish
         * controllers, name – it will be displayed as controller name.
         */

        StringWriter w = new StringWriter();
        w.append((char) 0x64);
        w.append((char) 0x00);
        writeBase64String(w, ip);
        writeBase64String(w, uniqueId);
        writeBase64String(w, appName);
        w.flush();
        return w.toString();
    }

    private void writeString(Writer writer, String str) throws IOException {
        int len = str.length();
        byte low = (byte) (len & 0xFF);
        byte high = (byte) ((len >> 8) & 0xFF);

        writer.append((char) (low));
        writer.append((char) (high));
        writer.append(str);
    }

    private void writeBase64String(Writer writer, String str) throws IOException {
        String tmp = new String(Base64.encodeBase64(str.getBytes()));
        writeString(writer, tmp);
    }

    private String readString(Reader reader) throws IOException {
        char[] buf = readCharArray(reader);
        return new String(buf);
    }

    private char[] readCharArray(Reader reader) throws IOException {
        byte low = (byte) reader.read();
        byte high = (byte) reader.read();
        int len = (high << 8) + low;

        if (len > 0) {
            char[] buffer = new char[len];
            reader.read(buffer);
            return buffer;
        } else {
            return new char[] {};
        }
    }

    private void sendKeyData(KeyCode key) throws RemoteControllerException {
        logger.debug("Sending key code {}", key.getValue());

        Writer localwriter = writer;
        Reader localreader = reader;
        if (localwriter == null || localreader == null) {
            return;
        }
        /* @formatter:off
         *
         * offset value and description
         * ------ ---------------------
         * 0x00   always 0x00
         * 0x01   0x0013 - string length (little endian)
         * 0x03   "iphone.iapp.samsung" - string content
         * 0x16   0x0011 - payload size (little endian)
         * 0x18   payload
         *
         * @formatter:on
         */
        try {
            localwriter.append((char) 0x00);
            writeString(localwriter, APP_STRING);
            writeString(localwriter, createKeyDataPayload(key));
            localwriter.flush();

            /*
             * Read response. Response is pretty useless, because TV seems to
             * send same response in both ok and error situation.
             */
            localreader.skip(1);
            readString(localreader);
            readCharArray(localreader);
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    private String createKeyDataPayload(KeyCode key) throws IOException {
        /* @formatter:off
        *
        * Payload:
        *
        * offset value and description
        * ------ ---------------------
        * 0x18   three 0x00 bytes
        * 0x1b   0x000c - key code size (little endian)
        * 0x1d   key code encoded as base64 string
        *
        * @formatter:on
        */

        StringWriter writer = new StringWriter();
        writer.append((char) 0x00);
        writer.append((char) 0x00);
        writer.append((char) 0x00);
        writeBase64String(writer, key.getValue());
        writer.flush();
        return writer.toString();
    }

    @Override
    public void close() throws RemoteControllerException {
        if (isConnected()) {
            closeConnection();
        }
    }
}
