/*
 * 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.
 */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <limits.h>
#include <string.h>
#include <fcntl.h>
#include <sys/poll.h>
#include <sys/ioctl.h>
#include <linux/dvb/dmx.h>
#include <linux/dvb/frontend.h>

#define LOG_TAG "DvbManager"
#include "logging.h"

#include "DvbManager.h"

static double currentTimeMillis() {
    struct timeval tv;
    gettimeofday(&tv, (struct timezone *) NULL);
    return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}

DvbManager::DvbManager(JNIEnv *env, jobject)
        : mFeFd(-1),
          mDvrFd(-1),
          mPatFilterFd(-1),
          mDvbApiVersion(DVB_API_VERSION_UNDEFINED),
          mDeliverySystemType(-1),
          mFeHasLock(false),
          mHasPendingTune(false) {
  jclass clazz = env->FindClass("com/android/tv/tuner/TunerHal");
  mOpenDvbFrontEndMethodID =
      env->GetMethodID(clazz, "openDvbFrontEndFd", "()I");
  mOpenDvbDemuxMethodID = env->GetMethodID(clazz, "openDvbDemuxFd", "()I");
  mOpenDvbDvrMethodID = env->GetMethodID(clazz, "openDvbDvrFd", "()I");
}

DvbManager::~DvbManager() {
    reset();
}

bool DvbManager::isFeLocked() {
    if (mDvbApiVersion == DVB_API_VERSION5) {
        fe_status_t status;
        if (ioctl(mFeFd, FE_READ_STATUS, &status) < 0) {
            return false;
        }
        if (status & FE_HAS_LOCK) {
            return true;
        }
    } else {
        struct pollfd pollFd;
        pollFd.fd = mFeFd;
        pollFd.events = POLLIN;
        pollFd.revents = 0;
        int poll_result = poll(&pollFd, NUM_POLLFDS, FE_POLL_TIMEOUT_MS);
        if (poll_result > 0 && (pollFd.revents & POLLIN)) {
            struct dvb_frontend_event kevent;
            memset(&kevent, 0, sizeof(kevent));
            if (ioctl(mFeFd, FE_GET_EVENT, &kevent) == 0) {
                return (kevent.status & FE_HAS_LOCK);
            }
        }
    }
    return false;
}

// This function gets the signal strength from tuner.
// Output can be:
// -3 means File Descriptor invalid,
//    or DVB version is not supported,
//    or ERROR while communicate with hardware via ioctl.
// int signal returns the raw signal strength value.
int DvbManager::getSignalStrength() {
    // TODO(b/74197177): add support for DVB V5.
    if (mFeFd == -1 || mDvbApiVersion != DVB_API_VERSION3) {
        return -3;
    }
    uint16_t strength = 0;
    // ERROR code from ioctl can be:
    // EBADF means fd is not a valid open file descriptor
    // EFAULT means status points to invalid address
    // ENOSIGNAL means there is no signal, thus no meaningful signal strength
    // ENOSYS means function not available for this device
    //
    // The function used to communicate with tuner in DVB v3 is
    // ioctl(fd, request, &strength)
    // int fd is the File Descriptor, can't be -1
    // int request is the request type,
    // FE_READ_SIGNAL_STRENGTH for getting signal strength
    // uint16_t *strength stores the strength value returned from tuner
    if (ioctl(mFeFd, FE_READ_SIGNAL_STRENGTH, &strength) == -1) {
        ALOGD("FE_READ_SIGNAL_STRENGTH failed, %s", strerror(errno));
        return -3;
    }
    return strength;
}

int DvbManager::tune(JNIEnv *env, jobject thiz,
        const int frequency, const char *modulationStr, int timeout_ms) {
    resetExceptFe();

    if (openDvbFe(env, thiz) != 0) {
        return -1;
    }

    if (frequency < 0) {
        return -1;
    }

    if (mDvbApiVersion == DVB_API_VERSION_UNDEFINED) {
        struct dtv_property testProps[1] = {
            { .cmd = DTV_DELIVERY_SYSTEM }
        };
        struct dtv_properties feProp = {
            .num = 1, .props = testProps
        };
        // On fugu, DVB_API_VERSION is 5 but it doesn't support FE_SET_PROPERTY. Checking the device
        // support FE_GET_PROPERTY or not to determine the DVB API version is greater than 5 or not.
        if (ioctl(mFeFd, FE_GET_PROPERTY, &feProp) == -1) {
            ALOGD("FE_GET_PROPERTY failed, %s", strerror(errno));
            mDvbApiVersion = DVB_API_VERSION3;
        } else {
            mDvbApiVersion = DVB_API_VERSION5;
        }
    }

    if (mDvbApiVersion == DVB_API_VERSION5) {
        struct dtv_property deliverySystemProperty = {
            .cmd = DTV_DELIVERY_SYSTEM
        };
        deliverySystemProperty.u.data = SYS_ATSC;
        struct dtv_property frequencyProperty = {
            .cmd = DTV_FREQUENCY
        };
        frequencyProperty.u.data = static_cast<__u32>(frequency);
        struct dtv_property modulationProperty = { .cmd = DTV_MODULATION };
        if (strncmp(modulationStr, "QAM", 3) == 0) {
            modulationProperty.u.data = QAM_AUTO;
        } else if (strcmp(modulationStr, "8VSB") == 0) {
            modulationProperty.u.data = VSB_8;
        } else {
            ALOGE("Unrecognized modulation mode : %s", modulationStr);
            return -1;
        }
        struct dtv_property tuneProperty = { .cmd = DTV_TUNE };

        struct dtv_property props[] = {
                deliverySystemProperty, frequencyProperty, modulationProperty, tuneProperty
        };
        struct dtv_properties dtvProperty = {
            .num = 4, .props = props
        };

        if (mHasPendingTune) {
            return -1;
        }
        if (ioctl(mFeFd, FE_SET_PROPERTY, &dtvProperty) != 0) {
            ALOGD("Can't set Frontend : %s", strerror(errno));
            return -1;
        }
    } else {
        struct dvb_frontend_parameters feParams;
        memset(&feParams, 0, sizeof(struct dvb_frontend_parameters));
        feParams.frequency = frequency;
        feParams.inversion = INVERSION_AUTO;
        /* Check frontend capability */
        struct dvb_frontend_info feInfo;
        if (ioctl(mFeFd, FE_GET_INFO, &feInfo) != -1) {
            if (!(feInfo.caps & FE_CAN_INVERSION_AUTO)) {
                // FE can't do INVERSION_AUTO, trying INVERSION_OFF instead
                feParams.inversion = INVERSION_OFF;
            }
        }
        switch (feInfo.type) {
            case FE_ATSC:
                if (strcmp(modulationStr, "8VSB") == 0) {
                    feParams.u.vsb.modulation = VSB_8;
                } else if (strncmp(modulationStr, "QAM", 3) == 0) {
                    feParams.u.vsb.modulation = QAM_AUTO;
                } else {
                    ALOGE("Unrecognized modulation mode : %s", modulationStr);
                    return -1;
                }
                break;
            case FE_OFDM:
                if (strcmp(modulationStr, "8VSB") == 0) {
                    feParams.u.ofdm.constellation = VSB_8;
                } else if (strcmp(modulationStr, "QAM16") == 0) {
                    feParams.u.ofdm.constellation = QAM_16;
                } else if (strcmp(modulationStr, "QAM64") == 0) {
                    feParams.u.ofdm.constellation = QAM_64;
                } else if (strcmp(modulationStr, "QAM256") == 0) {
                    feParams.u.ofdm.constellation = QAM_256;
                } else if (strcmp(modulationStr, "QPSK") == 0) {
                    feParams.u.ofdm.constellation = QPSK;
                } else {
                    ALOGE("Unrecognized modulation mode : %s", modulationStr);
                    return -1;
                }
                break;
            default:
                ALOGE("Unsupported delivery system.");
                return -1;
        }

        if (mHasPendingTune) {
            return -1;
        }

        if (ioctl(mFeFd, FE_SET_FRONTEND, &feParams) != 0) {
            ALOGD("Can't set Frontend : %s", strerror(errno));
            return -1;
        }
    }

    int lockSuccessCount = 0;
    double tuneClock = currentTimeMillis();
    while (currentTimeMillis() - tuneClock < timeout_ms) {
        if (mHasPendingTune) {
            // Return 0 here since we already call FE_SET_FRONTEND, and return due to having pending
            // tune request. And the frontend setting could be successful.
            mFeHasLock = true;
            return 0;
        }
        bool lockStatus = isFeLocked();
        if (lockStatus) {
            lockSuccessCount++;
        } else {
            lockSuccessCount = 0;
        }
        ALOGI("Lock status : %s", lockStatus ? "true" : "false");
        if (lockSuccessCount >= FE_CONSECUTIVE_LOCK_SUCCESS_COUNT) {
            mFeHasLock = true;
            openDvbDvr(env, thiz);
            return 0;
        }
    }

    return -1;
}

int DvbManager::stopTune() {
    reset();
    usleep(DVB_TUNE_STOP_DELAY_MS);
    return 0;
}

int DvbManager::openDvbFeFromSystemApi(JNIEnv *env, jobject thiz) {
    int fd = (int) env->CallIntMethod(thiz, mOpenDvbFrontEndMethodID);
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
    return fd;
}

int DvbManager::openDvbDemuxFromSystemApi(JNIEnv *env, jobject thiz) {
    int fd = (int) env->CallIntMethod(thiz, mOpenDvbDemuxMethodID);
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
    return fd;
}

int DvbManager::openDvbDvrFromSystemApi(JNIEnv *env, jobject thiz) {
    int fd = (int) env->CallIntMethod(thiz, mOpenDvbDvrMethodID);
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
    return fd;
}

int DvbManager::openDvbFe(JNIEnv *env, jobject thiz) {
    if (mFeFd == -1) {
        if ((mFeFd = openDvbFeFromSystemApi(env, thiz)) < 0) {
            ALOGD("Can't open FE file : %s", strerror(errno));
            return -1;
        }
    }

    struct dvb_frontend_info info;
    if (ioctl(mFeFd, FE_GET_INFO, &info) == 0) {
        const char *types;
        switch (info.type) {
            case FE_QPSK:
                types = "DVB-S";
                break;
            case FE_QAM:
                types = "DVB-C";
                break;
            case FE_OFDM:
                types = "DVB-T";
                break;
            case FE_ATSC:
                types = "ATSC";
                break;
            default:
                types = "Unknown";
        }
        ALOGI("Using frontend \"%s\", type %s", info.name, types);
    }
    return 0;
}

int DvbManager::startTsPidFilter(JNIEnv *env, jobject thiz, int pid, int filterType) {
    Mutex::Autolock autoLock(mFilterLock);

    if (mPidFilters.find(pid) != mPidFilters.end() || (mPatFilterFd != -1 && pid == PAT_PID)) {
        return 0;
    }

    if (mHasPendingTune) {
        return -1;
    }

    int demuxFd;
    if ((demuxFd = openDvbDemuxFromSystemApi(env, thiz)) < 0) {
        ALOGD("Can't open DEMUX file : %s", strerror(errno));
        return -1;
    }

    struct dmx_pes_filter_params filter;
    memset(&filter, 0, sizeof(filter));
    filter.pid = pid;
    filter.input = DMX_IN_FRONTEND;
    switch (filterType) {
        case FILTER_TYPE_AUDIO:
            filter.pes_type = DMX_PES_AUDIO;
            break;
        case FILTER_TYPE_VIDEO:
            filter.pes_type = DMX_PES_VIDEO;
            break;
        case FILTER_TYPE_PCR:
            filter.pes_type = DMX_PES_PCR;
            break;
        default:
            filter.pes_type = DMX_PES_OTHER;
            break;
    }
    filter.output = DMX_OUT_TS_TAP;
    filter.flags |= (DMX_CHECK_CRC | DMX_IMMEDIATE_START);

    // create a pes filter
    if (ioctl(demuxFd, DMX_SET_PES_FILTER, &filter)) {
        close(demuxFd);
        return -1;
    }

    if (mDvbApiVersion == DVB_API_VERSION5) {
        ioctl(demuxFd, DMX_START, 0);
    }

    if (pid != PAT_PID) {
        mPidFilters.insert(std::pair<int, int>(pid, demuxFd));
    } else {
        mPatFilterFd = demuxFd;
    }

    return 0;
}

void DvbManager::closeAllDvbPidFilter() {
    // Close all dvb pid filters except PAT filter to maintain the opening status of the device.
    Mutex::Autolock autoLock(mFilterLock);

    for (std::map<int, int>::iterator it(mPidFilters.begin());
                it != mPidFilters.end(); it++) {
        close(it->second);
    }
    mPidFilters.clear();
    // Close mDvrFd to make sure there is not buffer from previous channel left.
    closeDvbDvr();
}

void DvbManager::closePatFilter() {
    Mutex::Autolock autoLock(mFilterLock);

    if (mPatFilterFd != -1) {
        close(mPatFilterFd);
        mPatFilterFd = -1;
    }
}

int DvbManager::openDvbDvr(JNIEnv *env, jobject thiz) {
    if ((mDvrFd = openDvbDvrFromSystemApi(env, thiz)) < 0) {
        ALOGD("Can't open DVR file : %s", strerror(errno));
        return -1;
    }
    return 0;
}

void DvbManager::closeDvbFe() {
    if (mFeFd != -1) {
        close(mFeFd);
        mFeFd = -1;
    }
}

void DvbManager::closeDvbDvr() {
    if (mDvrFd != -1) {
        close(mDvrFd);
        mDvrFd = -1;
    }
}

void DvbManager::reset() {
    mFeHasLock = false;
    closeDvbDvr();
    closeAllDvbPidFilter();
    closePatFilter();
    closeDvbFe();
}

void DvbManager::resetExceptFe() {
    mFeHasLock = false;
    closeDvbDvr();
    closeAllDvbPidFilter();
    closePatFilter();
}

int DvbManager::readTsStream(JNIEnv *env, jobject thiz,
        uint8_t *tsBuffer, int tsBufferSize, int timeout_ms) {
    if (!mFeHasLock) {
        usleep(DVB_ERROR_RETRY_INTERVAL_MS);
        return -1;
    }

    if (mDvrFd == -1) {
        openDvbDvr(env, thiz);
    }

    struct pollfd pollFd;
    pollFd.fd = mDvrFd;
    pollFd.events = POLLIN|POLLPRI|POLLERR;
    pollFd.revents = 0;
    int poll_result = poll(&pollFd, NUM_POLLFDS, timeout_ms);
    if (poll_result == 0) {
        return 0;
    } else if (poll_result == -1 || pollFd.revents & POLLERR) {
        ALOGD("Can't read DVR : %s", strerror(errno));
        // TODO: Find how to recover this situation correctly.
        closeDvbDvr();
        usleep(DVB_ERROR_RETRY_INTERVAL_MS);
        return -1;
    }
    return read(mDvrFd, tsBuffer, tsBufferSize);
}

void DvbManager::setHasPendingTune(bool hasPendingTune) {
    mHasPendingTune = hasPendingTune;
}

int DvbManager::getDeliverySystemType(JNIEnv *env, jobject thiz) {
    if (mDeliverySystemType != -1) {
        return mDeliverySystemType;
    }
    if (mFeFd == -1) {
        if ((mFeFd = openDvbFeFromSystemApi(env, thiz)) < 0) {
            ALOGD("Can't open FE file : %s", strerror(errno));
            return DELIVERY_SYSTEM_UNDEFINED;
        }
    }
    struct dtv_property testProps[1] = {
        { .cmd = DTV_DELIVERY_SYSTEM }
    };
    struct dtv_properties feProp = {
        .num = 1, .props = testProps
    };
    mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
    if (ioctl(mFeFd, FE_GET_PROPERTY, &feProp) == -1) {
        mDvbApiVersion = DVB_API_VERSION3;
        if (openDvbFe(env, thiz) == 0) {
            struct dvb_frontend_info info;
            if (ioctl(mFeFd, FE_GET_INFO, &info) == 0) {
                switch (info.type) {
                    case FE_QPSK:
                        mDeliverySystemType = DELIVERY_SYSTEM_DVBS;
                        break;
                    case FE_QAM:
                        mDeliverySystemType = DELIVERY_SYSTEM_DVBC;
                        break;
                    case FE_OFDM:
                        mDeliverySystemType = DELIVERY_SYSTEM_DVBT;
                        break;
                    case FE_ATSC:
                        mDeliverySystemType = DELIVERY_SYSTEM_ATSC;
                        break;
                    default:
                        mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
                        break;
                }
            }
        }
    } else {
        mDvbApiVersion = DVB_API_VERSION5;
        switch (feProp.props[0].u.data) {
            case SYS_DVBT:
                mDeliverySystemType = DELIVERY_SYSTEM_DVBT;
                break;
            case SYS_DVBT2:
                mDeliverySystemType = DELIVERY_SYSTEM_DVBT2;
                break;
            case SYS_DVBS:
                mDeliverySystemType = DELIVERY_SYSTEM_DVBS;
                break;
            case SYS_DVBS2:
                mDeliverySystemType = DELIVERY_SYSTEM_DVBS2;
                break;
            case SYS_DVBC_ANNEX_A:
            case SYS_DVBC_ANNEX_B:
            case SYS_DVBC_ANNEX_C:
                mDeliverySystemType = DELIVERY_SYSTEM_DVBC;
                break;
            case SYS_ATSC:
                mDeliverySystemType = DELIVERY_SYSTEM_ATSC;
                break;
            default:
                mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
                break;
        }
    }
    return mDeliverySystemType;
}