/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.commands.hid;

import android.util.JsonReader;
import android.util.JsonToken;
import android.util.Log;
import android.util.SparseArray;

import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class Event {
    private static final String TAG = "HidEvent";

    public static final String COMMAND_REGISTER = "register";
    public static final String COMMAND_DELAY = "delay";
    public static final String COMMAND_REPORT = "report";

    // These constants come from "include/uapi/linux/input.h" in the kernel
    enum Bus {
        USB(0x03), BLUETOOTH(0x05);

        Bus(int value) {
            mValue = value;
        }

        int getValue() {
            return mValue;
        }

        private int mValue;
    }

    private int mId;
    private String mCommand;
    private String mName;
    private byte[] mDescriptor;
    private int mVid;
    private int mPid;
    private Bus mBus;
    private byte[] mReport;
    private SparseArray<byte[]> mFeatureReports;
    private Map<ByteBuffer, byte[]> mOutputs;
    private int mDuration;

    public int getId() {
        return mId;
    }

    public String getCommand() {
        return mCommand;
    }

    public String getName() {
        return mName;
    }

    public byte[] getDescriptor() {
        return mDescriptor;
    }

    public int getVendorId() {
        return mVid;
    }

    public int getProductId() {
        return mPid;
    }

    public int getBus() {
        return mBus.getValue();
    }

    public byte[] getReport() {
        return mReport;
    }

    public SparseArray<byte[]> getFeatureReports() {
        return mFeatureReports;
    }

    public Map<ByteBuffer, byte[]> getOutputs() {
        return mOutputs;
    }

    public int getDuration() {
        return mDuration;
    }

    public String toString() {
        return "Event{id=" + mId
            + ", command=" + String.valueOf(mCommand)
            + ", name=" + String.valueOf(mName)
            + ", descriptor=" + Arrays.toString(mDescriptor)
            + ", vid=" + mVid
            + ", pid=" + mPid
            + ", bus=" + mBus
            + ", report=" + Arrays.toString(mReport)
            + ", feature_reports=" + mFeatureReports.toString()
            + ", outputs=" + mOutputs.toString()
            + ", duration=" + mDuration
            + "}";
    }

    private static class Builder {
        private Event mEvent;

        public Builder() {
            mEvent = new Event();
        }

        public void setId(int id) {
            mEvent.mId = id;
        }

        private void setCommand(String command) {
            mEvent.mCommand = command;
        }

        public void setName(String name) {
            mEvent.mName = name;
        }

        public void setDescriptor(byte[] descriptor) {
            mEvent.mDescriptor = descriptor;
        }

        public void setReport(byte[] report) {
            mEvent.mReport = report;
        }

        public void setFeatureReports(SparseArray<byte[]> reports) {
            mEvent.mFeatureReports = reports;
        }

        public void setOutputs(Map<ByteBuffer, byte[]> outputs) {
            mEvent.mOutputs = outputs;
        }

        public void setVid(int vid) {
            mEvent.mVid = vid;
        }

        public void setPid(int pid) {
            mEvent.mPid = pid;
        }

        public void setBus(Bus bus) {
            mEvent.mBus = bus;
        }

        public void setDuration(int duration) {
            mEvent.mDuration = duration;
        }

        public Event build() {
            if (mEvent.mId == -1) {
                throw new IllegalStateException("No event id");
            } else if (mEvent.mCommand == null) {
                throw new IllegalStateException("Event does not contain a command");
            }
            if (COMMAND_REGISTER.equals(mEvent.mCommand)) {
                if (mEvent.mDescriptor == null) {
                    throw new IllegalStateException("Device registration is missing descriptor");
                }
            } else if (COMMAND_DELAY.equals(mEvent.mCommand)) {
                if (mEvent.mDuration <= 0) {
                    throw new IllegalStateException("Delay has missing or invalid duration");
                }
            } else if (COMMAND_REPORT.equals(mEvent.mCommand)) {
                if (mEvent.mReport == null) {
                    throw new IllegalStateException("Report command is missing report data");
                }
            }
            return mEvent;
        }
    }

    public static class Reader {
        private JsonReader mReader;

        public Reader(InputStreamReader in) {
            mReader = new JsonReader(in);
            mReader.setLenient(true);
        }

        public Event getNextEvent() throws IOException {
            Event e = null;
            while (e == null && mReader.peek() != JsonToken.END_DOCUMENT) {
                Event.Builder eb = new Event.Builder();
                try {
                    mReader.beginObject();
                    while (mReader.hasNext()) {
                        String name = mReader.nextName();
                        switch (name) {
                            case "id":
                                eb.setId(readInt());
                                break;
                            case "command":
                                eb.setCommand(mReader.nextString());
                                break;
                            case "descriptor":
                                eb.setDescriptor(readData());
                                break;
                            case "name":
                                eb.setName(mReader.nextString());
                                break;
                            case "vid":
                                eb.setVid(readInt());
                                break;
                            case "pid":
                                eb.setPid(readInt());
                                break;
                            case "bus":
                                eb.setBus(readBus());
                                break;
                            case "report":
                                eb.setReport(readData());
                                break;
                            case "feature_reports":
                                eb.setFeatureReports(readFeatureReports());
                                break;
                            case "outputs":
                                eb.setOutputs(readOutputs());
                                break;
                            case "duration":
                                eb.setDuration(readInt());
                                break;
                            default:
                                mReader.skipValue();
                        }
                    }
                    mReader.endObject();
                } catch (IllegalStateException ex) {
                    error("Error reading in object, ignoring.", ex);
                    consumeRemainingElements();
                    mReader.endObject();
                    continue;
                }
                e = eb.build();
            }

            return e;
        }

        private byte[] readData() throws IOException {
            ArrayList<Integer> data = new ArrayList<Integer>();
            try {
                mReader.beginArray();
                while (mReader.hasNext()) {
                    data.add(Integer.decode(mReader.nextString()));
                }
                mReader.endArray();
            } catch (IllegalStateException|NumberFormatException e) {
                consumeRemainingElements();
                mReader.endArray();
                throw new IllegalStateException("Encountered malformed data.", e);
            }
            byte[] rawData = new byte[data.size()];
            for (int i = 0; i < data.size(); i++) {
                int d = data.get(i);
                if ((d & 0xFF) != d) {
                    throw new IllegalStateException("Invalid data, all values must be byte-sized");
                }
                rawData[i] = (byte)d;
            }
            return rawData;
        }

        private int readInt() throws IOException {
            String val = mReader.nextString();
            return Integer.decode(val);
        }

        private Bus readBus() throws IOException {
            String val = mReader.nextString();
            return Bus.valueOf(val.toUpperCase());
        }

        private SparseArray<byte[]> readFeatureReports()
                throws IllegalStateException, IOException {
            SparseArray<byte[]> featureReports = new SparseArray<>();
            try {
                mReader.beginArray();
                while (mReader.hasNext()) {
                    // If "id" is not specified, it defaults to 0, which means
                    // report does not contain report ID (based on HID specs).
                    int id = 0;
                    byte[] data = null;
                    mReader.beginObject();
                    while (mReader.hasNext()) {
                        String name = mReader.nextName();
                        switch (name) {
                            case "id":
                                id = readInt();
                                break;
                            case "data":
                                data = readData();
                                break;
                            default:
                                consumeRemainingElements();
                                mReader.endObject();
                                throw new IllegalStateException("Invalid key in feature report: "
                                        + name);
                        }
                    }
                    mReader.endObject();
                    if (data != null) {
                        featureReports.put(id, data);
                    }
                }
                mReader.endArray();
            } catch (IllegalStateException | NumberFormatException e) {
                consumeRemainingElements();
                mReader.endArray();
                throw new IllegalStateException("Encountered malformed data.", e);
            }
            return featureReports;
        }

        private Map<ByteBuffer, byte[]> readOutputs()
                throws IllegalStateException, IOException {
            Map<ByteBuffer, byte[]> outputs = new HashMap<>();

            try {
                mReader.beginArray();
                while (mReader.hasNext()) {
                    byte[] output = null;
                    byte[] response = null;
                    mReader.beginObject();
                    while (mReader.hasNext()) {
                        String name = mReader.nextName();
                        switch (name) {
                            case "description":
                                // Description is only used to keep track of the output responses
                                mReader.nextString();
                                break;
                            case "output":
                                output = readData();
                                break;
                            case "response":
                                response = readData();
                                break;
                            default:
                                consumeRemainingElements();
                                mReader.endObject();
                                throw new IllegalStateException("Invalid key in outputs: " + name);
                        }
                    }
                    mReader.endObject();
                    if (output != null) {
                        outputs.put(ByteBuffer.wrap(output), response);
                    }
                }
                mReader.endArray();
            } catch (IllegalStateException | NumberFormatException e) {
                consumeRemainingElements();
                mReader.endArray();
                throw new IllegalStateException("Encountered malformed data.", e);
            }
            return outputs;
        }

        private void consumeRemainingElements() throws IOException {
            while (mReader.hasNext()) {
                mReader.skipValue();
            }
        }
    }

    private static void error(String msg, Exception e) {
        System.out.println(msg);
        Log.e(TAG, msg);
        if (e != null) {
            Log.e(TAG, Log.getStackTraceString(e));
        }
    }
}
