/*
 * Copyright (C) 2019 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.settings.development;

import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.android.settings.R;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import javax.net.ssl.HttpsURLConnection;

/**
 * DSU Loader is a front-end that offers developers the ability to boot into GSI with one-click. It
 * also offers the flexibility to overwrite the default setting and load OEMs owned images.
 */
public class DSULoader extends ListActivity {
    private static final int Q_VNDK_BASE = 28;
    private static final int Q_OS_BASE = 10;

    private static final boolean DEBUG = false;
    private static final String TAG = "DSULOADER";
    private static final String PROPERTY_KEY_CPU = "ro.product.cpu.abi";
    private static final String PROPERTY_KEY_OS = "ro.system.build.version.release";
    private static final String PROPERTY_KEY_VNDK = "ro.vndk.version";
    private static final String PROPERTY_KEY_LIST =
            "persist.sys.fflag.override.settings_dynamic_system.list";
    private static final String PROPERTY_KEY_SPL = "ro.build.version.security_patch";
    private static final String DSU_LIST =
            "https://dl.google.com/developers/android/gsi/gsi-src.json";

    private static final int TIMEOUT_MS = 10 * 1000;
    private List<Object> mDSUList = new ArrayList<Object>();
    private ArrayAdapter<Object> mAdapter;

    private static String readAll(InputStream in) throws IOException {
        int n;
        StringBuilder list = new StringBuilder();
        byte[] bytes = new byte[4096];
        while ((n = in.read(bytes, 0, 4096)) != -1) {
            list.append(new String(Arrays.copyOf(bytes, n)));
        }
        return list.toString();
    }

    private static String readAll(URL url) throws IOException {
        InputStream in = null;
        HttpsURLConnection connection = null;
        Slog.i(TAG, "fetch " + url.toString());
        try {
            connection = (HttpsURLConnection) url.openConnection();
            connection.setReadTimeout(TIMEOUT_MS);
            connection.setConnectTimeout(TIMEOUT_MS);
            connection.setRequestMethod("GET");
            connection.setDoInput(true);
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) {
                throw new IOException("HTTP error code: " + responseCode);
            }
            in = new BufferedInputStream(connection.getInputStream());
            return readAll(in);
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                if (in != null) {
                    in.close();
                    in = null;
                }
            } catch (IOException e) {
                // ignore
            }
            if (connection != null) {
                connection.disconnect();
                connection = null;
            }
        }
    }

    private void resizeListView() {
        ListView view = getListView();
        View item_view = mAdapter.getView(0, null, view);
        item_view.measure(
                MeasureSpec.makeMeasureSpec(view.getWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        ViewGroup.LayoutParams params = view.getLayoutParams();
        params.height = item_view.getMeasuredHeight() * (mAdapter.getCount() + 1);
        Slog.e(TAG, "resizeListView height=" + params.height);
        view.setLayoutParams(params);
        view.requestLayout();
    }

    // Fetcher fetches mDSUList in backgroud
    private class Fetcher implements Runnable {
        private URL mDsuList;

        Fetcher(URL dsuList) {
            mDsuList = dsuList;
        }

        private void fetch(URL url)
                throws IOException, JSONException, MalformedURLException, ParseException {
            String content = readAll(url);
            JSONObject jsn = new JSONObject(content);
            // The include primitive is like below
            // "include": [
            //   "https:/...json",
            //    ...
            // ]
            if (jsn.has("include")) {
                JSONArray include = jsn.getJSONArray("include");
                int len = include.length();
                for (int i = 0; i < len; i++) {
                    if (include.isNull(i)) {
                        continue;
                    }
                    fetch(new URL(include.getString(i)));
                }
            }
            //  "images":[
            //    {
            //      "name":"...",
            //      "os_version":"10",
            //      "cpu_abi":"...",
            //      "details":"...",
            //      "vndk":[],
            //      "spl":"...",
            //      "pubkey":"",
            //      "uri":"https://...zip"
            //    },
            //     ...
            //  ]
            if (jsn.has("images")) {
                JSONArray images = jsn.getJSONArray("images");
                int len = images.length();
                for (int i = 0; i < len; i++) {
                    DSUPackage dsu = new DSUPackage(images.getJSONObject(i));
                    if (dsu.isSupported()) {
                        mDSUList.add(dsu);
                    }
                }
            }
        }

        public void run() {
            try {
                fetch(mDsuList);
                if (mDSUList.size() == 0) {
                    mDSUList.add(0, "No DSU available for this device");
                }
            } catch (IOException e) {
                Slog.e(TAG, e.toString());
                mDSUList.add(0, "Network Error");
            } catch (Exception e) {
                Slog.e(TAG, e.toString());
                mDSUList.add(0, "Metadata Error");
            }
            runOnUiThread(
                    new Runnable() {
                        public void run() {
                            mAdapter.clear();
                            mAdapter.addAll(mDSUList);
                            resizeListView();
                        }
                    });
        }
    }

    private class DSUPackage {
        private static final String NAME = "name";
        private static final String DETAILS = "details";
        private static final String CPU_ABI = "cpu_abi";
        private static final String URI = "uri";
        private static final String OS_VERSION = "os_version";
        private static final String VNDK = "vndk";
        private static final String PUBKEY = "pubkey";
        private static final String SPL = "spl";
        private static final String SPL_FORMAT = "yyyy-MM-dd";
        private static final String TOS = "tos";

        String mName = null;
        String mDetails = null;
        String mCpuAbi = null;
        int mOsVersion = -1;
        int[] mVndk = null;
        String mPubKey = "";
        Date mSPL = null;
        URL mTosUrl = null;
        URL mUri;

        DSUPackage(JSONObject jsn) throws JSONException, MalformedURLException, ParseException {
            Slog.i(TAG, "DSUPackage: " + jsn.toString());
            mName = jsn.getString(NAME);
            mDetails = jsn.getString(DETAILS);
            mCpuAbi = jsn.getString(CPU_ABI);
            mUri = new URL(jsn.getString(URI));
            if (jsn.has(OS_VERSION)) {
                mOsVersion = dessertNumber(jsn.getString(OS_VERSION), Q_OS_BASE);
            }
            if (jsn.has(VNDK)) {
                JSONArray vndks = jsn.getJSONArray(VNDK);
                mVndk = new int[vndks.length()];
                for (int i = 0; i < vndks.length(); i++) {
                    mVndk[i] = vndks.getInt(i);
                }
            }
            if (jsn.has(PUBKEY)) {
                mPubKey = jsn.getString(PUBKEY);
            }
            if (jsn.has(TOS)) {
                mTosUrl = new URL(jsn.getString(TOS));
            }
            if (jsn.has(SPL)) {
                mSPL = new SimpleDateFormat(SPL_FORMAT).parse(jsn.getString(SPL));
            }
        }

        int dessertNumber(String s, int base) {
            if (s == null || s.isEmpty()) {
                return -1;
            }
            if (Character.isDigit(s.charAt(0))) {
                return Integer.parseInt(s);
            } else {
                s = s.toUpperCase();
                return ((int) s.charAt(0) - (int) 'Q') + base;
            }
        }

        int getDeviceVndk() {
            if (DEBUG) {
                return Q_VNDK_BASE;
            }
            return dessertNumber(SystemProperties.get(PROPERTY_KEY_VNDK), Q_VNDK_BASE);
        }

        int getDeviceOs() {
            if (DEBUG) {
                return Q_OS_BASE;
            }
            return dessertNumber(SystemProperties.get(PROPERTY_KEY_OS), Q_OS_BASE);
        }

        String getDeviceCpu() {
            String cpu = SystemProperties.get(PROPERTY_KEY_CPU);
            cpu = cpu.toLowerCase();
            if (cpu.startsWith("aarch64")) {
                cpu = "arm64-v8a";
            }
            return cpu;
        }

        Date getDeviceSPL() {
            String spl = SystemProperties.get(PROPERTY_KEY_SPL);
            if (TextUtils.isEmpty(spl)) {
                return null;
            }
            try {
                return new SimpleDateFormat(SPL_FORMAT).parse(spl);
            } catch (ParseException e) {
                return null;
            }
        }

        boolean isSupported() {
            boolean supported = true;
            String cpu = getDeviceCpu();
            if (!mCpuAbi.equals(cpu)) {
                Slog.i(TAG, mCpuAbi + " != " + cpu);
                supported = false;
            }
            if (mOsVersion > 0) {
                int os = getDeviceOs();
                if (os < 0) {
                    Slog.i(TAG, "Failed to getDeviceOs");
                    supported = false;
                } else if (mOsVersion < os) {
                    Slog.i(TAG, mOsVersion + " < " + os);
                    supported = false;
                }
            }
            if (mVndk != null) {
                int vndk = getDeviceVndk();
                if (vndk < 0) {
                    Slog.i(TAG, "Failed to getDeviceVndk");
                    supported = false;
                } else {
                    boolean found_vndk = false;
                    for (int i = 0; i < mVndk.length; i++) {
                        if (mVndk[i] == vndk) {
                            found_vndk = true;
                            break;
                        }
                    }
                    if (!found_vndk) {
                        Slog.i(TAG, "vndk:" + vndk + " not found");
                        supported = false;
                    }
                }
            }
            if (mSPL != null) {
                Date spl = getDeviceSPL();
                if (spl == null) {
                    Slog.i(TAG, "Failed to getDeviceSPL");
                    supported = false;
                } else if (spl.getTime() > mSPL.getTime()) {
                    Slog.i(TAG, "Device SPL:" + spl.toString() + " > " + mSPL.toString());
                    supported = false;
                }
            }
            Slog.i(TAG, mName + " isSupported " + supported);
            return supported;
        }
    }

    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        String dsuList = SystemProperties.get(PROPERTY_KEY_LIST);
        Slog.e(TAG, "Try to get DSU list from: " + PROPERTY_KEY_LIST);
        if (dsuList == null || dsuList.isEmpty()) {
            dsuList = DSU_LIST;
        }
        Slog.e(TAG, "DSU list: " + dsuList);
        URL url = null;
        try {
            url = new URL(dsuList);
        } catch (MalformedURLException e) {
            Slog.e(TAG, e.toString());
            return;
        }
        mAdapter = new DSUPackageListAdapter(this);
        setListAdapter(mAdapter);
        mAdapter.add(getResources().getString(R.string.dsu_loader_loading));
        new Thread(new Fetcher(url)).start();
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        Object selected = mAdapter.getItem(position);
        if (selected instanceof DSUPackage) {
            DSUPackage dsu = (DSUPackage) selected;
            mAdapter.clear();
            mAdapter.add(getResources().getString(R.string.dsu_loader_loading));
            new Thread(new Runnable() {
                public void run() {
                    String termsOfService = "";
                    if (dsu.mTosUrl != null) {
                        try {
                            termsOfService = readAll(dsu.mTosUrl);
                        } catch (IOException e) {
                            Slog.e(TAG, e.toString());
                        }
                    }
                    Intent intent = new Intent(DSULoader.this, DSUTermsOfServiceActivity.class);
                    intent.putExtra(DSUTermsOfServiceActivity.KEY_TOS, termsOfService);
                    intent.setData(Uri.parse(dsu.mUri.toString()));
                    intent.putExtra("KEY_PUBKEY", dsu.mPubKey);
                    startActivity(intent);
                }
            }).start();
        }
        finish();
    }

    private class DSUPackageListAdapter extends ArrayAdapter<Object> {
        private final LayoutInflater mInflater;

        DSUPackageListAdapter(Context context) {
            super(context, 0);
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            AppViewHolder holder = AppViewHolder.createOrRecycle(mInflater, convertView);
            convertView = holder.rootView;
            Object item = getItem(position);
            if (item instanceof DSUPackage) {
                DSUPackage dsu = (DSUPackage) item;
                holder.appName.setText(dsu.mName);
                holder.summary.setText(dsu.mDetails);
            } else {
                String msg = (String) item;
                holder.summary.setText(msg);
            }
            holder.appIcon.setImageDrawable(null);
            holder.disabled.setVisibility(View.GONE);
            return convertView;
        }
    }
}
