/*
 * This file is part of the libCEC(R) library.
 *
 * libCEC(R) is Copyright (C) 2011-2026 Pulse-Eight Limited.  All rights reserved.
 * libCEC(R) is an original work, containing original code.
 *
 * libCEC(R) is a trademark of Pulse-Eight Limited.
 *
 * This program is dual-licensed; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 *
 *
 * Alternatively, you can license this library under a commercial license,
 * please contact Pulse-Eight Licensing for more information.
 *
 * For more information contact:
 * Pulse-Eight Licensing       <license@pulse-eight.com>
 *     http://www.pulse-eight.com/
 *     http://www.pulse-eight.net/
 */

#include "env.h"
#include "SLCommandHandler.h"

#include "platform/util/timeutils.h"
#include "devices/CECBusDevice.h"
#include "devices/CECPlaybackDevice.h"
#include "CECProcessor.h"
#include "LibCEC.h"
#include <stdio.h>

using namespace CEC;

#define SL_COMMAND_TYPE_HDDRECORDER_DISC  0x01
#define SL_COMMAND_TYPE_VCR               0x02
#define SL_COMMAND_TYPE_DVDPLAYER         0x03
#define SL_COMMAND_TYPE_HDDRECORDER_DISC2 0x04
#define SL_COMMAND_TYPE_HDDRECORDER       0x05

#define SL_COMMAND_INIT                 0x01
#define SL_COMMAND_ACK_INIT             0x02
#define SL_COMMAND_POWER_ON             0x03
#define SL_COMMAND_CONNECT_REQUEST      0x04
#define SL_COMMAND_SET_DEVICE_MODE      0x05
#define SL_COMMAND_REQUEST_RECONNECT    0x0b
#define SL_COMMAND_REQUEST_POWER_STATUS 0xa0

/* how long the TV's own routing changes are ignored after it powered on */
#define SL_IGNORE_TV_ROUTE_TIMEOUT_MS   30000
/* how long to let the TV settle before telling it which source to show */
#define SL_TV_ROUTE_SETTLE_MS           1000
/* how long the TV takes to switch inputs after it powers a source on */
#define SL_POWER_ON_SETTLE_MS           3000

#define LIB_CEC     m_busDevice->GetProcessor()->GetLib()
#define ToString(p) LIB_CEC->ToString(p)

CSLCommandHandler::CSLCommandHandler(CCECBusDevice *busDevice,
                                     int32_t iTransmitTimeout /* = CEC_DEFAULT_TRANSMIT_TIMEOUT */,
                                     int32_t iTransmitWait /* = CEC_DEFAULT_TRANSMIT_WAIT */,
                                     int8_t iTransmitRetries /* = CEC_DEFAULT_TRANSMIT_RETRIES */,
                                     int64_t iActiveSourcePending /* = 0 */) :
    CCECCommandHandler(busDevice, iTransmitTimeout, iTransmitWait, iTransmitRetries, iActiveSourcePending),
    m_bSLEnabled(false)
{
  m_vendorId = CEC_VENDOR_LG;

  /* LG devices don't always reply to CEC version requests, so just set it to the version
     that libCEC claims everywhere else */
  m_busDevice->SetCecVersion(CEC_VERSION_1_4);

  /* LG devices always return "korean" as language */
  cec_menu_language lang;
  snprintf(lang, 4, "eng");
  m_busDevice->SetMenuLanguage(lang);
}

bool CSLCommandHandler::InitHandler(void)
{
  if (m_bHandlerInited)
    return true;
  m_bHandlerInited = true;

  if (m_busDevice->GetLogicalAddress() != CECDEVICE_TV)
    return true;

  IgnoreTvRoutingChanges();

  CCECBusDevice *primary = m_processor->GetPrimaryDevice();
  if (primary && primary->GetLogicalAddress() != CECDEVICE_UNREGISTERED)
  {
    /* imitate LG devices */
    if (m_busDevice->GetLogicalAddress() != primary->GetLogicalAddress())
    {
      primary->SetVendorId(CEC_VENDOR_LG);
      primary->ReplaceHandler(false);
    }
  }

  return true;
}

int CSLCommandHandler::HandleVendorCommand(const cec_command &command)
{
  if (!m_processor->IsHandledByLibCEC(command.destination) && command.destination != CECDEVICE_BROADCAST)
    return COMMAND_HANDLED;

  if (command.parameters.size == 1 &&
      command.parameters[0] == SL_COMMAND_INIT)
  {
    HandleVendorCommandSLInit(command);
    return COMMAND_HANDLED;
  }
  else if (command.parameters.size == 2 &&
      command.parameters[0] == SL_COMMAND_POWER_ON)
  {
    HandleVendorCommandPowerOn(command, true);
    return COMMAND_HANDLED;
  }
  else if (command.parameters.size == 2 &&
      command.parameters[0] == SL_COMMAND_CONNECT_REQUEST)
  {
    HandleVendorCommandSLConnect(command);
    return COMMAND_HANDLED;
  }
  else if (command.parameters.size == 1 &&
      command.parameters[0] == SL_COMMAND_REQUEST_RECONNECT)
  {
    HandleVendorCommandPowerOnStatus(command);
    return COMMAND_HANDLED;
  }
  else if (command.parameters.size == 1 &&
      command.parameters[0] == SL_COMMAND_REQUEST_POWER_STATUS)
  {
    HandleVendorCommandPowerOnStatus(command);
    return COMMAND_HANDLED;
  }

  return CCECCommandHandler::HandleVendorCommand(command);
}

void CSLCommandHandler::HandleVendorCommandSLInit(const cec_command &command)
{
  /* the TV starts the SimpLink handshake when it powers on */
  IgnoreTvRoutingChanges();

  CCECBusDevice* dev = m_processor->GetDevice(command.destination);
  if (dev && dev->IsHandledByLibCEC())
  {
    if (!dev->IsActiveSource())
    {
      dev->SetPowerStatus(CEC_POWER_STATUS_STANDBY);
      dev->TransmitPowerState(command.initiator, true);
    }

    TransmitVendorCommandSLAckInit(command.destination, command.initiator);
  }
}

void CSLCommandHandler::TransmitVendorCommandSLAckInit(const cec_logical_address iSource, const cec_logical_address iDestination)
{
  cec_command response;
  cec_command::Format(response, iSource, iDestination, CEC_OPCODE_VENDOR_COMMAND);
  response.PushBack(SL_COMMAND_ACK_INIT);
  response.PushBack(SL_COMMAND_TYPE_HDDRECORDER);

  Transmit(response, false, true);
  SetSLInitialised();
}

void CSLCommandHandler::HandleVendorCommandPowerOn(const cec_command &command, bool activateSource)
{
  if (command.initiator != CECDEVICE_TV)
    return;

  /* the TV powers on the source it wants to show */
  IgnoreTvRoutingChanges();

  CCECBusDevice *device = m_processor->GetPrimaryDevice();
  if (device)
  {
    bool wasActive = device->IsActiveSource();
    SetSLInitialised();
    device->MarkAsActiveSource();
    device->SetPowerStatus(CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON);
    device->TransmitPowerState(command.initiator, true);

    /* answer 'on' from here on, which is what the TV asks for while it switches inputs */
    device->SetPowerStatus(CEC_POWER_STATUS_ON);
    device->TransmitPhysicalAddress(false);

    /* claim the source once the TV has had time to switch to it. the processor sends it when
       the delay expires, so the bus keeps being served until then */
    if (!wasActive || activateSource)
    {
      device->GetHandler()->ScheduleActivateSource(SL_POWER_ON_SETTLE_MS);
      device->MarkHandlerReady();
    }
  }
}
void CSLCommandHandler::HandleVendorCommandPowerOnStatus(const cec_command &command)
{
  if (command.destination != CECDEVICE_BROADCAST)
  {
    CCECBusDevice *device = m_processor->GetPrimaryDevice();
    if (device)
    {
      device->SetPowerStatus(CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON);
      device->TransmitPowerState(command.initiator, true);
      device->SetPowerStatus(CEC_POWER_STATUS_ON);
    }
  }
}

void CSLCommandHandler::HandleVendorCommandSLConnect(const cec_command &command)
{
  SetSLInitialised();
  TransmitVendorCommandSetDeviceMode(command.destination, command.initiator, CEC_DEVICE_TYPE_RECORDING_DEVICE);


  if (m_processor->IsActiveSource(command.destination) && m_processor->IsHandledByLibCEC(command.destination))
  {
    CCECBusDevice* dev = m_processor->GetDevice(command.destination);
    CCECPlaybackDevice* pb = dev->AsPlaybackDevice();
    if (pb)
      pb->TransmitDeckStatus(command.initiator, true);
    dev->TransmitPowerState(command.initiator, true);
  }
}

void CSLCommandHandler::TransmitVendorCommandSetDeviceMode(const cec_logical_address iSource, const cec_logical_address iDestination, const cec_device_type type)
{
  cec_command response;
  cec_command::Format(response, iSource, iDestination, CEC_OPCODE_VENDOR_COMMAND);
  response.PushBack(SL_COMMAND_SET_DEVICE_MODE);
  response.PushBack((uint8_t)type);
  Transmit(response, false, true);
}

int CSLCommandHandler::HandleGiveDeckStatus(const cec_command &command)
{
  if (!m_processor->CECInitialised() ||
      !m_processor->IsHandledByLibCEC(command.destination))
    return CEC_ABORT_REASON_NOT_IN_CORRECT_MODE_TO_RESPOND;

  CCECPlaybackDevice *device = CCECBusDevice::AsPlaybackDevice(GetDevice(command.destination));
  if (!device || command.parameters.size == 0)
    return CEC_ABORT_REASON_INVALID_OPERAND;

  /* LG's own deck status code marks the source that the TV is showing, so a device that
     isn't the active source reports that it stopped and leaves the source where it is */
  if (!device->IsActiveSource())
  {
    device->SetDeckStatus(CEC_DECK_INFO_STOP);
    return CCECCommandHandler::HandleGiveDeckStatus(command);
  }

  device->SetDeckStatus(CEC_DECK_INFO_OTHER_STATUS_LG);
  if (command.parameters[0] == CEC_STATUS_REQUEST_ON)
  {
    device->TransmitDeckStatus(command.initiator, true);
    ActivateSource();
    return COMMAND_HANDLED;
  }
  else if (command.parameters[0] == CEC_STATUS_REQUEST_ONCE)
  {
    device->TransmitDeckStatus(command.initiator, true);
    return COMMAND_HANDLED;
  }

  return CCECCommandHandler::HandleGiveDeckStatus(command);
}

int CSLCommandHandler::HandleGiveDevicePowerStatus(const cec_command &command)
{
  if (!m_processor->CECInitialised() ||
      !m_processor->IsHandledByLibCEC(command.destination) ||
      command.initiator != CECDEVICE_TV)
    return CEC_ABORT_REASON_NOT_IN_CORRECT_MODE_TO_RESPOND;

  CCECBusDevice *device = GetDevice(command.destination);
  if (!device)
    return CEC_ABORT_REASON_NOT_IN_CORRECT_MODE_TO_RESPOND;

  /* the TV picks the source it wants with the vendor 'power on' command, and gets confused
     when a device that it isn't showing also claims to be powered on */
  if (!device->IsActiveSource())
  {
    device->SetPowerStatus(CEC_POWER_STATUS_STANDBY);
    device->TransmitPowerState(command.initiator, true);
    return COMMAND_HANDLED;
  }

  if (m_resetPowerState.IsSet() && m_resetPowerState.TimeLeft() > 0)
  {
    /* the TV re-asks within seconds when it isn't getting a picture.
       most likely our physical address is wrong: it falls back to 1.0.0.0 when it can't be
       read from the EDID, which leaves the set on an input that has no signal.
       repeating 'on' makes the set drop the source, so stall it with 'in transition' */
    LIB_CEC->AddLog(CEC_LOG_WARNING, "the TV isn't getting a signal at %04x - it keeps asking whether %s (%X) is powered on. check that the physical address is correct. reporting 'in transition standby to on'; the return button will not work",
                    device->GetCurrentPhysicalAddress(), device->GetLogicalAddressName(), device->GetLogicalAddress());
    device->SetPowerStatus(CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON);
    device->TransmitPowerState(command.initiator, true);
    device->SetPowerStatus(CEC_POWER_STATUS_ON);
  }
  else
  {
    device->SetPowerStatus(CEC_POWER_STATUS_ON);
    device->TransmitPowerState(command.initiator, true);
  }
  m_resetPowerState.Init(5000);

  return COMMAND_HANDLED;
}

int CSLCommandHandler::HandleRequestActiveSource(const cec_command &command)
{
  if (m_processor->CECInitialised())
  {
    if (!SLInitialised())
      TransmitVendorCommandSLAckInit(m_processor->GetPrimaryDevice()->GetLogicalAddress(), command.initiator);
    CCECCommandHandler::HandleRequestActiveSource(command);
  }
  return CEC_ABORT_REASON_NOT_IN_CORRECT_MODE_TO_RESPOND;
}

int CSLCommandHandler::HandleFeatureAbort(const cec_command &command)
{
  CCECBusDevice* primary = m_processor->GetPrimaryDevice();
  if (command.parameters.size == 0 && primary->GetLogicalAddress() != CECDEVICE_UNKNOWN &&
      primary->GetCurrentPowerStatus() == CEC_POWER_STATUS_ON && !SLInitialised() &&
      command.initiator == CECDEVICE_TV)
  {
    if (!SLInitialised() && m_processor->IsActiveSource(command.destination))
    {
      TransmitVendorCommandSLAckInit(command.destination, command.initiator);
      return COMMAND_HANDLED;
    }
  }

  return CCECCommandHandler::HandleFeatureAbort(command);
}

int CSLCommandHandler::HandleStandby(const cec_command &command)
{
  ResetSLState();

  return CCECCommandHandler::HandleStandby(command);
}

int CSLCommandHandler::HandleActiveSource(const cec_command &command)
{
  if (SuppressTvRoutingChange(command))
    return COMMAND_HANDLED;

  return CCECCommandHandler::HandleActiveSource(command);
}

int CSLCommandHandler::HandleSetStreamPath(const cec_command &command)
{
  if (SuppressTvRoutingChange(command))
    return COMMAND_HANDLED;

  return CCECCommandHandler::HandleSetStreamPath(command);
}

void CSLCommandHandler::IgnoreTvRoutingChanges(void)
{
  CLockObject lock(m_SLMutex);
  m_ignoreTvRoute.Init(SL_IGNORE_TV_ROUTE_TIMEOUT_MS);
}

bool CSLCommandHandler::SuppressTvRoutingChange(const cec_command &command)
{
  if (command.initiator != CECDEVICE_TV)
    return false;

  {
    CLockObject lock(m_SLMutex);
    if (m_ignoreTvRoute.TimeLeft() == 0)
      return false;
  }

  /* an LG routes to its own address while it powers on, which lands the user on input 1
     instead of the source they were watching. keep the source that is active and tell the
     TV about it once it has settled */
  CCECBusDevice* activeSource = m_processor->GetDevices()->GetActiveSource();
  if (!activeSource || !activeSource->IsHandledByLibCEC())
    return false;

  LIB_CEC->AddLog(CEC_LOG_DEBUG, "ignoring the TV's routing change, keeping %s (%X) as the active source", activeSource->GetLogicalAddressName(), activeSource->GetLogicalAddress());
  activeSource->GetHandler()->ScheduleActivateSource(SL_TV_ROUTE_SETTLE_MS);
  activeSource->MarkHandlerReady();

  return true;
}

void CSLCommandHandler::ResetSLState(void)
{
  LIB_CEC->AddLog(CEC_LOG_NOTICE, "resetting SL initialised state");
  CLockObject lock(m_SLMutex);
  m_bSLEnabled = false;
  m_processor->GetPrimaryDevice()->SetPowerStatus(CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON);
}

void CSLCommandHandler::SetSLInitialised(void)
{
  LIB_CEC->AddLog(CEC_LOG_NOTICE, "SL initialised");
  CLockObject lock(m_SLMutex);
  m_bSLEnabled = true;
}

bool CSLCommandHandler::SLInitialised(void)
{
  CLockObject lock(m_SLMutex);
  return m_bSLEnabled;
}

bool CSLCommandHandler::PowerOn(const cec_logical_address iInitiator, const cec_logical_address iDestination)
{
  if (iDestination != CECDEVICE_TV)
  {
    /* LG devices only allow themselves to be woken up by the TV with a vendor command */
    cec_command command;

    if (!m_bSLEnabled)
      TransmitVendorID(CECDEVICE_TV, iDestination, CEC_VENDOR_LG, false);

    cec_command::Format(command, CECDEVICE_TV, iDestination, CEC_OPCODE_VENDOR_COMMAND);
    command.PushBack(SL_COMMAND_POWER_ON);
    command.PushBack(0);
    return Transmit(command, false, false);
  }

  return CCECCommandHandler::PowerOn(iInitiator, iDestination);
}

bool CSLCommandHandler::TransmitPlay(const cec_logical_address iInitiator, const cec_logical_address iDestination, cec_play_mode mode)
{
  /* LG players ignore <play> and only act on the equivalent user control code */
  cec_user_control_code key;
  switch (mode)
  {
  case CEC_PLAY_MODE_PLAY_STILL:
    key = CEC_USER_CONTROL_CODE_PAUSE;
    break;
  case CEC_PLAY_MODE_PLAY_REVERSE:
  case CEC_PLAY_MODE_FAST_REVERSE_MIN_SPEED:
  case CEC_PLAY_MODE_FAST_REVERSE_MEDIUM_SPEED:
  case CEC_PLAY_MODE_FAST_REVERSE_MAX_SPEED:
  case CEC_PLAY_MODE_SLOW_REVERSE_MIN_SPEED:
  case CEC_PLAY_MODE_SLOW_REVERSE_MEDIUM_SPEED:
  case CEC_PLAY_MODE_SLOW_REVERSE_MAX_SPEED:
    key = CEC_USER_CONTROL_CODE_BACKWARD;
    break;
  default:
    key = CEC_USER_CONTROL_CODE_PLAY;
    break;
  }

  return TransmitKeypress(iInitiator, iDestination, key) &&
      TransmitKeyRelease(iInitiator, iDestination);
}

bool CSLCommandHandler::ActivateSource(bool bTransmitDelayedCommandsOnly /* = false */)
{
  if (m_busDevice->IsActiveSource() &&
      m_busDevice->IsHandledByLibCEC())
  {
    {
      CLockObject lock(m_mutex);
      // check if we need to send a delayed source switch
      if (bTransmitDelayedCommandsOnly)
      {
        if (m_iActiveSourcePending == 0 || GetTimeMs() < m_iActiveSourcePending)
          return false;

#ifdef CEC_DEBUGGING
        LIB_CEC->AddLog(CEC_LOG_DEBUG, "transmitting delayed activate source command");
#endif
      }
    }

    CCECPlaybackDevice *device = m_busDevice->AsPlaybackDevice();
    if (device)
      device->SetDeckStatus(!device->IsActiveSource() ? CEC_DECK_INFO_OTHER_STATUS : CEC_DECK_INFO_OTHER_STATUS_LG);

    // power on the TV
    CCECBusDevice* tv = m_processor->GetDevice(CECDEVICE_TV);
    bool bTvPresent = (tv && tv->GetStatus() == CEC_DEVICE_STATUS_PRESENT);
    bool bActiveSourceFailed(false);
    if (bTvPresent)
    {
      bActiveSourceFailed = !!device && !device->TransmitImageViewOn();
    }
    else
    {
      LIB_CEC->AddLog(CEC_LOG_DEBUG, "TV not present, not sending 'image view on'");
    }

    // check if we're allowed to switch sources
    bool bSourceSwitchAllowed = SourceSwitchAllowed();
    if (!bSourceSwitchAllowed)
      LIB_CEC->AddLog(CEC_LOG_DEBUG, "source switch is currently not allowed by command handler");

    // switch sources (if allowed)
    if (!bActiveSourceFailed && bSourceSwitchAllowed)
    {
      bActiveSourceFailed = !m_busDevice->TransmitActiveSource(false);
    }

    // retry later
    if (bActiveSourceFailed || !bSourceSwitchAllowed)
    {
      LIB_CEC->AddLog(CEC_LOG_DEBUG, "failed to make '%s' the active source. will retry later", m_busDevice->GetLogicalAddressName());
      int64_t now(GetTimeMs());
      CLockObject lock(m_mutex);
      if (m_iActiveSourcePending == 0 || m_iActiveSourcePending < now)
        m_iActiveSourcePending = now + (int64_t)CEC_ACTIVE_SOURCE_SWITCH_RETRY_TIME_MS;
      return false;
    }
    else
    {
      CLockObject lock(m_mutex);
      // clear previous pending active source command
      m_iActiveSourcePending = 0;
    }

    // mark the handler as initialised
    CLockObject lock(m_mutex);
    m_bHandlerInited = true;
  }
  return true;
}
