/*
 * Copyright (C) 2017 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.
 */

#define LOG_TAG "BroadcastRadioDefault.tuner"
#define LOG_NDEBUG 0

#include "Tuner.h"
#include "BroadcastRadio.h"

#include <broadcastradio-utils-1x/Utils.h>
#include <log/log.h>

namespace android {
namespace hardware {
namespace broadcastradio {
namespace V1_1 {
namespace implementation {

using namespace std::chrono_literals;

using V1_0::Band;
using V1_0::BandConfig;
using V1_0::Class;
using V1_0::Direction;
using V1_1::IdentifierType;
using V1_1::ProgramInfo;
using V1_1::ProgramInfoFlags;
using V1_1::ProgramListResult;
using V1_1::ProgramSelector;
using V1_1::ProgramType;
using V1_1::VendorKeyValue;
using utils::HalRevision;

using std::chrono::milliseconds;
using std::lock_guard;
using std::move;
using std::mutex;
using std::sort;
using std::vector;

const struct {
    milliseconds config = 50ms;
    milliseconds scan = 200ms;
    milliseconds step = 100ms;
    milliseconds tune = 150ms;
} gDefaultDelay;

Tuner::Tuner(const sp<BroadcastRadio> module, V1_0::Class classId,
             const sp<V1_0::ITunerCallback>& callback)
    : mModule(module),
      mClassId(classId),
      mCallback(callback),
      mCallback1_1(V1_1::ITunerCallback::castFrom(callback).withDefault(nullptr)),
      mVirtualRadio(getRadio(classId)),
      mIsAnalogForced(false) {}

void Tuner::forceClose() {
    lock_guard<mutex> lk(mMut);
    mIsClosed = true;
    mThread.cancelAll();
}

void Tuner::setConfigurationInternalLocked(const BandConfig& config) {
    mAmfmConfig = config;
    mAmfmConfig.antennaConnected = true;
    mCurrentProgram = utils::make_selector(mAmfmConfig.type, mAmfmConfig.lowerLimit);

    if (utils::isFm(mAmfmConfig.type)) {
        mVirtualRadio = std::ref(getFmRadio());
    } else {
        mVirtualRadio = std::ref(getAmRadio());
    }

    mIsAmfmConfigSet = true;
    mCallback->configChange(Result::OK, mAmfmConfig);
    if (mCallback1_1 != nullptr) mCallback1_1->programListChanged();
}

bool Tuner::autoConfigureLocked(uint64_t frequency) {
    for (auto&& config : mModule->getAmFmBands()) {
        // The check here is rather poor, but it's enough for default implementation.
        if (config.lowerLimit <= frequency && config.upperLimit >= frequency) {
            ALOGI("Auto-switching band to %s", toString(config).c_str());
            setConfigurationInternalLocked(config);
            return true;
        }
    }
    return false;
}

Return<Result> Tuner::setConfiguration(const BandConfig& config) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;
    if (mClassId != Class::AM_FM) {
        ALOGE("Can't set AM/FM configuration on SAT/DT radio tuner");
        return Result::INVALID_STATE;
    }

    if (config.lowerLimit >= config.upperLimit) return Result::INVALID_ARGUMENTS;

    auto task = [this, config]() {
        ALOGI("Setting AM/FM config");
        lock_guard<mutex> lk(mMut);
        setConfigurationInternalLocked(config);
    };
    mThread.schedule(task, gDefaultDelay.config);

    return Result::OK;
}

Return<void> Tuner::getConfiguration(getConfiguration_cb _hidl_cb) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);

    if (!mIsClosed && mIsAmfmConfigSet) {
        _hidl_cb(Result::OK, mAmfmConfig);
    } else {
        _hidl_cb(Result::NOT_INITIALIZED, {});
    }
    return {};
}

// makes ProgramInfo that points to no program
static ProgramInfo makeDummyProgramInfo(const ProgramSelector& selector) {
    ProgramInfo info11 = {};
    auto& info10 = info11.base;

    utils::getLegacyChannel(selector, &info10.channel, &info10.subChannel);
    info11.selector = selector;
    info11.flags |= ProgramInfoFlags::MUTED;

    return info11;
}

HalRevision Tuner::getHalRev() const {
    if (mCallback1_1 != nullptr) {
        return HalRevision::V1_1;
    } else {
        return HalRevision::V1_0;
    }
}

void Tuner::tuneInternalLocked(const ProgramSelector& sel) {
    VirtualProgram virtualProgram;
    if (mVirtualRadio.get().getProgram(sel, virtualProgram)) {
        mCurrentProgram = virtualProgram.selector;
        mCurrentProgramInfo = virtualProgram.getProgramInfo(getHalRev());
    } else {
        mCurrentProgram = sel;
        mCurrentProgramInfo = makeDummyProgramInfo(sel);
    }
    mIsTuneCompleted = true;

    if (mCallback1_1 == nullptr) {
        mCallback->tuneComplete(Result::OK, mCurrentProgramInfo.base);
    } else {
        mCallback1_1->tuneComplete_1_1(Result::OK, mCurrentProgramInfo.selector);
        mCallback1_1->currentProgramInfoChanged(mCurrentProgramInfo);
    }
}

Return<Result> Tuner::scan(Direction direction, bool skipSubChannel __unused) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    auto list = mVirtualRadio.get().getProgramList();

    if (list.empty()) {
        mIsTuneCompleted = false;
        auto task = [this, direction]() {
            ALOGI("Performing failed scan %s", toString(direction).c_str());

            if (mCallback1_1 == nullptr) {
                mCallback->tuneComplete(Result::TIMEOUT, {});
            } else {
                mCallback1_1->tuneComplete_1_1(Result::TIMEOUT, {});
            }
        };
        mThread.schedule(task, gDefaultDelay.scan);

        return Result::OK;
    }

    // Not optimal (O(sort) instead of O(n)), but not a big deal here;
    // also, it's likely that list is already sorted (so O(n) anyway).
    sort(list.begin(), list.end());
    auto current = mCurrentProgram;
    auto found = lower_bound(list.begin(), list.end(), VirtualProgram({current}));
    if (direction == Direction::UP) {
        if (found < list.end() - 1) {
            if (utils::tunesTo(current, found->selector)) found++;
        } else {
            found = list.begin();
        }
    } else {
        if (found > list.begin() && found != list.end()) {
            found--;
        } else {
            found = list.end() - 1;
        }
    }
    auto tuneTo = found->selector;

    mIsTuneCompleted = false;
    auto task = [this, tuneTo, direction]() {
        ALOGI("Performing scan %s", toString(direction).c_str());

        lock_guard<mutex> lk(mMut);
        tuneInternalLocked(tuneTo);
    };
    mThread.schedule(task, gDefaultDelay.scan);

    return Result::OK;
}

Return<Result> Tuner::step(Direction direction, bool skipSubChannel) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    ALOGW_IF(!skipSubChannel, "can't step to next frequency without ignoring subChannel");

    if (!utils::isAmFm(utils::getType(mCurrentProgram))) {
        ALOGE("Can't step in anything else than AM/FM");
        return Result::NOT_INITIALIZED;
    }

    if (!mIsAmfmConfigSet) {
        ALOGW("AM/FM config not set");
        return Result::INVALID_STATE;
    }
    mIsTuneCompleted = false;

    auto task = [this, direction]() {
        ALOGI("Performing step %s", toString(direction).c_str());

        lock_guard<mutex> lk(mMut);

        auto current = utils::getId(mCurrentProgram, IdentifierType::AMFM_FREQUENCY, 0);

        if (direction == Direction::UP) {
            current += mAmfmConfig.spacings[0];
        } else {
            current -= mAmfmConfig.spacings[0];
        }

        if (current > mAmfmConfig.upperLimit) current = mAmfmConfig.lowerLimit;
        if (current < mAmfmConfig.lowerLimit) current = mAmfmConfig.upperLimit;

        tuneInternalLocked(utils::make_selector(mAmfmConfig.type, current));
    };
    mThread.schedule(task, gDefaultDelay.step);

    return Result::OK;
}

Return<Result> Tuner::tune(uint32_t channel, uint32_t subChannel) {
    ALOGV("%s(%d, %d)", __func__, channel, subChannel);
    Band band;
    {
        lock_guard<mutex> lk(mMut);
        band = mAmfmConfig.type;
    }
    return tuneByProgramSelector(utils::make_selector(band, channel, subChannel));
}

Return<Result> Tuner::tuneByProgramSelector(const ProgramSelector& sel) {
    ALOGV("%s(%s)", __func__, toString(sel).c_str());
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    // checking if ProgramSelector is valid
    auto programType = utils::getType(sel);
    if (utils::isAmFm(programType)) {
        if (!mIsAmfmConfigSet) {
            ALOGW("AM/FM config not set");
            return Result::INVALID_STATE;
        }

        auto freq = utils::getId(sel, IdentifierType::AMFM_FREQUENCY);
        if (freq < mAmfmConfig.lowerLimit || freq > mAmfmConfig.upperLimit) {
            if (!autoConfigureLocked(freq)) return Result::INVALID_ARGUMENTS;
        }
    } else if (programType == ProgramType::DAB) {
        if (!utils::hasId(sel, IdentifierType::DAB_SIDECC)) return Result::INVALID_ARGUMENTS;
    } else if (programType == ProgramType::DRMO) {
        if (!utils::hasId(sel, IdentifierType::DRMO_SERVICE_ID)) return Result::INVALID_ARGUMENTS;
    } else if (programType == ProgramType::SXM) {
        if (!utils::hasId(sel, IdentifierType::SXM_SERVICE_ID)) return Result::INVALID_ARGUMENTS;
    } else {
        return Result::INVALID_ARGUMENTS;
    }

    mIsTuneCompleted = false;
    auto task = [this, sel]() {
        lock_guard<mutex> lk(mMut);
        tuneInternalLocked(sel);
    };
    mThread.schedule(task, gDefaultDelay.tune);

    return Result::OK;
}

Return<Result> Tuner::cancel() {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    mThread.cancelAll();
    return Result::OK;
}

Return<Result> Tuner::cancelAnnouncement() {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    return Result::OK;
}

Return<void> Tuner::getProgramInformation(getProgramInformation_cb _hidl_cb) {
    ALOGV("%s", __func__);
    return getProgramInformation_1_1(
        [&](Result result, const ProgramInfo& info) { _hidl_cb(result, info.base); });
}

Return<void> Tuner::getProgramInformation_1_1(getProgramInformation_1_1_cb _hidl_cb) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);

    if (mIsClosed) {
        _hidl_cb(Result::NOT_INITIALIZED, {});
    } else if (mIsTuneCompleted) {
        _hidl_cb(Result::OK, mCurrentProgramInfo);
    } else {
        _hidl_cb(Result::NOT_INITIALIZED, makeDummyProgramInfo(mCurrentProgram));
    }
    return {};
}

Return<ProgramListResult> Tuner::startBackgroundScan() {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return ProgramListResult::NOT_INITIALIZED;

    if (mCallback1_1 != nullptr) {
        mCallback1_1->backgroundScanComplete(ProgramListResult::OK);
    }

    return ProgramListResult::OK;
}

Return<void> Tuner::getProgramList(const hidl_vec<VendorKeyValue>& vendorFilter,
                                   getProgramList_cb _hidl_cb) {
    ALOGV("%s(%s)", __func__, toString(vendorFilter).substr(0, 100).c_str());
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) {
        _hidl_cb(ProgramListResult::NOT_INITIALIZED, {});
        return {};
    }

    auto list = mVirtualRadio.get().getProgramList();
    ALOGD("returning a list of %zu programs", list.size());
    _hidl_cb(ProgramListResult::OK, getProgramInfoVector(list, getHalRev()));
    return {};
}

Return<Result> Tuner::setAnalogForced(bool isForced) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);
    if (mIsClosed) return Result::NOT_INITIALIZED;

    mIsAnalogForced = isForced;
    return Result::OK;
}

Return<void> Tuner::isAnalogForced(isAnalogForced_cb _hidl_cb) {
    ALOGV("%s", __func__);
    lock_guard<mutex> lk(mMut);

    if (mIsClosed) {
        _hidl_cb(Result::NOT_INITIALIZED, false);
    } else {
        _hidl_cb(Result::OK, mIsAnalogForced);
    }
    return {};
}

}  // namespace implementation
}  // namespace V1_1
}  // namespace broadcastradio
}  // namespace hardware
}  // namespace android
