/*
 *
 *    Copyright (c) 2020-2022 Project CHIP Authors
 *
 *    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.
 */

use super::objects::*;
use crate::{
    error::*,
    interaction_model::{
        command::CommandReq,
        core::IMStatusCode,
        messages::ib::{self},
    },
    tlv::{TLVWriter, TagType, ToTLV},
};
use chrono::{DateTime, NaiveDate};
use num_derive::FromPrimitive;

pub const ID: u32 = 0x0506;
#[derive(FromPrimitive)]
pub enum Attributes {
    CurrentState = 0x0,
    StartTime = 0x1,
    Duration = 0x2,
    SampledPosition = 0x3,
    PlaybackSpeed = 0x4,
    SeekRangeEnd = 0x5,
    SeekRangeStart = 0x6,
}

struct ClusterCallback {
    name: Commands,
    callback: Box<dyn FnMut()>,
}

enum _FeatureMap {
    AdvancedSeek = 0,
    VariableSpeed = 1,
}
#[derive(FromPrimitive)]

enum PlaybackState {
    Playing = 0,
    Paused = 1,
    NotPlaying = 2,
    BUFFERING = 3,
}
#[derive(FromPrimitive)]
enum CommandStatus {
    Success = 0,
    InvalidStateForCommand = 1,
    NotAllowed = 2,
    NotActive = 3,
    SpeedOutOfRange = 4,
    SeekOutOfRange = 5,
}

impl CommandStatus {
    fn u8(&self) -> u8 {
        match self {
            CommandStatus::Success => 0,
            CommandStatus::InvalidStateForCommand => 1,
            CommandStatus::NotAllowed => 2,
            CommandStatus::NotActive => 3,
            CommandStatus::SpeedOutOfRange => 4,
            CommandStatus::SeekOutOfRange => 5,
        }
    }
}

#[derive(FromPrimitive, PartialEq)]
pub enum Commands {
    Play = 0x0,
    Pause = 0x1,
    Stop = 0x2,
    StartOver = 0x3,
    Previous = 0x4,
    Next = 0x5,
    Rewind = 0x6,
    FastForward = 0x7,
    SkipForward = 0x8,
    SkipBackward = 0x9,
    // Response is from us to server
    PlaybackResponse = 0xa,
    Seek = 0x0b,
}

struct PlaybackPosition {
    updated_at: u64,
    position: u64,
}
// Get microseconds since 2000, Jan 1, 00:00:00
pub fn get_epoch_us() -> u64 {
    let epoch_start = NaiveDate::from_ymd_opt(2000, 1, 1)
        .unwrap()
        .and_hms_micro_opt(0, 0, 0, 0)
        .unwrap()
        .and_local_timezone(chrono::Utc)
        .unwrap();
    DateTime::timestamp_micros(&epoch_start) as u64
}

pub struct MediaPlaybackCluster {
    base: Cluster,
    sampled_position: PlaybackPosition,
    callbacks: Vec<ClusterCallback>,
}

impl MediaPlaybackCluster {
    pub fn new() -> Result<Box<Self>, Error> {
        let mut cluster = Box::new(MediaPlaybackCluster {
            base: Cluster::new(ID)?,
            sampled_position: PlaybackPosition {
                updated_at: 0,
                position: 0,
            },
            callbacks: vec![],
        });

        // List should be a Vec<
        let attrs = [
            Attribute::new(
                Attributes::CurrentState as u16,
                AttrValue::Uint8(PlaybackState::NotPlaying as u8),
                Access::RV,
                Quality::PERSISTENT,
            )?,
            // epoch-us
            Attribute::new(
                Attributes::StartTime as u16,
                AttrValue::Uint64(0),
                Access::RV,
                Quality::PERSISTENT,
            )?,
            Attribute::new(
                Attributes::Duration as u16,
                AttrValue::Uint64(1),
                Access::RV,
                Quality::PERSISTENT,
            )?,
            // Playback-Position
            Attribute::new(
                Attributes::SampledPosition as u16,
                AttrValue::Custom,
                Access::RV,
                Quality::PERSISTENT,
            )?,
            // Float
            Attribute::new(
                Attributes::PlaybackSpeed as u16,
                AttrValue::Custom,
                Access::RV,
                Quality::PERSISTENT,
            )?,
            Attribute::new(
                Attributes::SeekRangeEnd as u16,
                AttrValue::Uint64(0),
                Access::RV,
                Quality::PERSISTENT,
            )?,
            Attribute::new(
                Attributes::SeekRangeStart as u16,
                AttrValue::Uint64(0),
                Access::RV,
                Quality::PERSISTENT,
            )?,
            // Options - probably want a custom type here for mapping cluster options to TLV bitmask
        ];
        cluster.base.add_attributes(&attrs)?;

        // For now disable all features by default
        cluster.base.set_feature_map(0)?;
        Ok(cluster)
    }

    pub fn add_callback(&mut self, name: Commands, callback: Box<dyn FnMut()>) {
        self.callbacks.push(ClusterCallback {
            name,
            callback: callback,
        });
    }

    fn run_callback(&mut self, name: Commands) {
        for cmd in self.callbacks.iter_mut() {
            if cmd.name == name {
                (cmd.callback)()
            }
        }
    }

    fn _set_state_buffering(&mut self) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        Err(IMStatusCode::Sucess)
    }

    fn _set_duration(&mut self, duration: u64) -> Result<(), IMStatusCode> {
        self.base
            .write_attribute_raw(Attributes::Duration as u16, AttrValue::Uint64(duration))?;
        Err(IMStatusCode::Sucess)
    }

    // When rewinding / changing stream / etc we need to change absolute position and updateAt
    fn update_position(&mut self, new_pos: u64) -> Result<(), IMStatusCode> {
        let now = get_epoch_us();

        self.sampled_position.position = new_pos;
        self.sampled_position.updated_at = now;

        Err(IMStatusCode::Sucess)
    }

    fn enocde_sampled_position(&self, tag: TagType, tw: &mut TLVWriter) {
        let _ = tw.start_struct(tag);
        let _ = tw.u64(TagType::Context(0), self.sampled_position.position);
        let _ = tw.u64(TagType::Context(1), self.sampled_position.updated_at);
        let _ = tw.end_container();
    }
}

// Commmands
impl MediaPlaybackCluster {
    fn handle_play(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;

        self.run_callback(Commands::Play);
        self.send_playback_response(CommandStatus::Success, cmd_req);
        Err(IMStatusCode::Sucess)
    }
    fn handle_pause(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Paused as u8),
        )?;

        self.run_callback(Commands::Pause);
        self.send_playback_response(CommandStatus::Success, cmd_req);
        Err(IMStatusCode::Sucess)
    }
    fn handle_stop(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::NotPlaying as u8),
        )?;

        self.run_callback(Commands::Stop);
        self.send_playback_response(CommandStatus::Success, cmd_req);
        Err(IMStatusCode::Sucess)
    }

    // Start current thinbg over
    fn handle_start_over(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;

        self.update_position(0)?;
        self.run_callback(Commands::StartOver);
        self.send_playback_response(CommandStatus::Success, cmd_req);
        Err(IMStatusCode::Sucess)
    }

    fn handle_next(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        // self.update_position(0)?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_previous(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        // self.update_position(0)?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_rewind(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_ff(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_skip_forward(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_skip_backward(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    fn handle_seek(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        self.base.write_attribute_raw(
            Attributes::CurrentState as u16,
            AttrValue::Uint8(PlaybackState::Playing as u8),
        )?;
        self.send_playback_response(CommandStatus::NotAllowed, cmd_req);
        Err(IMStatusCode::UnsupportedCommand)
    }

    // TODO: We send this to client
    fn send_playback_response(&mut self, status: CommandStatus, cmd_req: &mut CommandReq) {
        let mut playback_response = cmd_req.cmd;
        playback_response.path.leaf = Some(Commands::PlaybackResponse as u32);

        let resp = status.u8();
        let cmd_data = |tag: TagType, t: &mut TLVWriter| {
            let _ = t.start_struct(tag);
            let _ = t.u8(TagType::Context(0), resp);
            let _ = t.end_container();
        };

        let invoke_resp = ib::InvResp::Cmd(ib::CmdData::new(
            playback_response,
            EncodeValue::Closure(&cmd_data),
        ));
        let _ = invoke_resp.to_tlv(cmd_req.resp, TagType::Anonymous);
        cmd_req.trans.complete();
    }
}

impl ClusterType for MediaPlaybackCluster {
    fn base(&self) -> &Cluster {
        &self.base
    }
    fn base_mut(&mut self) -> &mut Cluster {
        &mut self.base
    }

    fn read_custom_attribute(&self, encoder: &mut dyn Encoder, attr: &AttrDetails) {
        match num::FromPrimitive::from_u16(attr.attr_id) {
            Some(Attributes::SampledPosition) => {
                encoder.encode(EncodeValue::Closure(&|tag, tw| {
                    log::warn!("Encoding sampled position of self!");
                    self.enocde_sampled_position(tag, tw)
                }))
            }
            _ => log::error!("Attribute not supported!"),
        }
    }

    fn handle_command(&mut self, cmd_req: &mut CommandReq) -> Result<(), IMStatusCode> {
        let cmd = cmd_req
            .cmd
            .path
            .leaf
            .map(num::FromPrimitive::from_u32)
            .ok_or(IMStatusCode::UnsupportedCommand)?
            .ok_or(IMStatusCode::UnsupportedCommand)?;

        match cmd {
            Commands::Play => self.handle_play(cmd_req),
            Commands::Pause => self.handle_pause(cmd_req),
            Commands::Stop => self.handle_stop(cmd_req),
            Commands::StartOver => self.handle_start_over(cmd_req),
            Commands::Previous => self.handle_previous(cmd_req),
            Commands::Next => self.handle_next(cmd_req),
            Commands::Rewind => self.handle_rewind(cmd_req),
            Commands::FastForward => self.handle_ff(cmd_req),
            Commands::SkipForward => self.handle_skip_forward(cmd_req),
            Commands::SkipBackward => self.handle_skip_backward(cmd_req),
            Commands::PlaybackResponse => Err(IMStatusCode::InvalidCommand),
            Commands::Seek => self.handle_seek(cmd_req),
        }
    }
}