/*
 * Copyright (C) 2014 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.ddmlib;

import com.android.annotations.Nullable;
import com.google.common.util.concurrent.SettableFuture;

import java.io.IOException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Fetches battery level from device.
 */
class BatteryFetcher {

    private static final String LOG_TAG = "BatteryFetcher";

    /** the amount of time to wait between unsuccessful battery fetch attempts */
    private static final long FETCH_BACKOFF_MS = 5 * 1000; // 5 seconds
    private static final long BATTERY_TIMEOUT = 2 * 1000; // 2 seconds

    /**
     * Output receiver for "cat /sys/class/power_supply/.../capacity" command line.
     */
    static final class SysFsBatteryLevelReceiver extends MultiLineReceiver {

        private static final Pattern BATTERY_LEVEL = Pattern.compile("^(\\d+)[.\\s]*");
        private Integer mBatteryLevel = null;

        /**
         * Get the parsed battery level.
         * @return battery level or <code>null</code> if it cannot be determined
         */
        @Nullable
        public Integer getBatteryLevel() {
            return mBatteryLevel;
        }

        @Override
        public boolean isCancelled() {
            return false;
        }

        @Override
        public void processNewLines(String[] lines) {
            for (String line : lines) {
                Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
                if (batteryMatch.matches()) {
                    if (mBatteryLevel == null) {
                        mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
                    } else {
                        // multiple matches, check if they are different
                        Integer tmpLevel = Integer.parseInt(batteryMatch.group(1));
                        if (!mBatteryLevel.equals(tmpLevel)) {
                            Log.w(LOG_TAG, String.format(
                                    "Multiple lines matched with different value; " +
                                    "Original: %s, Current: %s (keeping original)",
                                    mBatteryLevel.toString(), tmpLevel.toString()));
                        }
                    }
                }
            }
        }
    }

    /**
     * Output receiver for "dumpsys battery" command line.
     */
    private static final class BatteryReceiver extends MultiLineReceiver {
        private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)");
        private static final Pattern SCALE = Pattern.compile("\\s*scale: (\\d+)");

        private Integer mBatteryLevel = null;
        private Integer mBatteryScale = null;

        /**
         * Get the parsed percent battery level.
         * @return
         */
        public Integer getBatteryLevel() {
            if (mBatteryLevel != null && mBatteryScale != null) {
                return (mBatteryLevel * 100) / mBatteryScale;
            }
            return null;
        }

        @Override
        public void processNewLines(String[] lines) {
            for (String line : lines) {
                Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
                if (batteryMatch.matches()) {
                    try {
                        mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
                    } catch (NumberFormatException e) {
                        Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
                                batteryMatch.group(1)));
                    }
                }
                Matcher scaleMatch = SCALE.matcher(line);
                if (scaleMatch.matches()) {
                    try {
                        mBatteryScale = Integer.parseInt(scaleMatch.group(1));
                    } catch (NumberFormatException e) {
                        Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
                                batteryMatch.group(1)));
                    }
                }
            }
        }

        @Override
        public boolean isCancelled() {
            return false;
        }
    }

    private Integer mBatteryLevel = null;
    private final IDevice mDevice;
    private long mLastSuccessTime = 0;
    private SettableFuture<Integer> mPendingRequest = null;

    public BatteryFetcher(IDevice device) {
        mDevice = device;
    }

    /**
     * Make a possibly asynchronous request for the device's battery level
     *
     * @param freshness the desired recentness of battery level
     * @param timeUnit the {@link TimeUnit} of freshness
     * @return a {@link Future} that can be used to retrieve the battery level
     */
    public synchronized Future<Integer> getBattery(long freshness, TimeUnit timeUnit) {
        SettableFuture<Integer> result;
        if (mBatteryLevel == null || isFetchRequired(freshness, timeUnit)) {
            if (mPendingRequest == null) {
                // no request underway - start a new one
                mPendingRequest = SettableFuture.create();
                initiateBatteryQuery();
            } else {
                // fall through - return the already created future from the request already
                // underway
            }
            result = mPendingRequest;
        } else {
            // cache is populated within desired freshness
            result = SettableFuture.create();
            result.set(mBatteryLevel);
        }
        return result;
    }

    private boolean isFetchRequired(long freshness, TimeUnit timeUnit) {
        long freshnessMs = timeUnit.toMillis(freshness);
        return (System.currentTimeMillis() - mLastSuccessTime) > freshnessMs;
    }

    private void initiateBatteryQuery() {
        String threadName = String.format("query-battery-%s", mDevice.getSerialNumber());
        Thread fetchThread = new Thread(threadName) {
            @Override
            public void run() {
                Exception exception = null;
                try {
                    // first try to get it from sysfs
                    SysFsBatteryLevelReceiver sysBattReceiver = new SysFsBatteryLevelReceiver();
                    mDevice.executeShellCommand("cat /sys/class/power_supply/*/capacity",
                            sysBattReceiver, BATTERY_TIMEOUT, TimeUnit.MILLISECONDS);
                    if (!setBatteryLevel(sysBattReceiver.getBatteryLevel())) {
                        // failed! try dumpsys
                        BatteryReceiver receiver = new BatteryReceiver();
                        mDevice.executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT,
                                TimeUnit.MILLISECONDS);
                        if (setBatteryLevel(receiver.getBatteryLevel())) {
                            return;
                        }
                    } else {
                        return;
                    }
                    exception = new IOException("Unrecognized response to battery level queries");
                } catch (TimeoutException e) {
                    exception = e;
                } catch (AdbCommandRejectedException e) {
                    exception = e;
                } catch (ShellCommandUnresponsiveException e) {
                    exception = e;
                } catch (IOException e) {
                    exception = e;
                }
                handleBatteryLevelFailure(exception);
            }
        };
        fetchThread.setDaemon(true);
        fetchThread.start();
    }

    private synchronized boolean setBatteryLevel(Integer batteryLevel) {
        if (batteryLevel == null) {
            return false;
        }
        mLastSuccessTime = System.currentTimeMillis();
        mBatteryLevel = batteryLevel;
        if (mPendingRequest != null) {
            mPendingRequest.set(mBatteryLevel);
        }
        mPendingRequest = null;
        return true;
    }

    private synchronized void handleBatteryLevelFailure(Exception e) {
        Log.w(LOG_TAG, String.format(
                "%s getting battery level for device %s: %s",
                e.getClass().getSimpleName(), mDevice.getSerialNumber(), e.getMessage()));
        if (mPendingRequest != null) {
            if (!mPendingRequest.setException(e)) {
                // should never happen
                Log.e(LOG_TAG, "Future.setException failed");
                mPendingRequest.set(null);
            }
        }
        mPendingRequest = null;
    }
}
