//
//  json.cpp
//
//  Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
//
//  This program is free software: 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 3 of the License, or
//  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, see <http://www.gnu.org/licenses/>.
//

#include <boost/foreach.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <iostream>
#include <regex>
#include <string>

#include "log.hpp"
#include "utils.hpp"
#include "json.hpp"

static inline std::string remove_undesired_chars(const std::string& s) {
  std::regex html_regex("[^ A-Za-z0-9:~._/=%\()\\r\\n\\t\?#-]?");
  return std::regex_replace(s, html_regex, "");
}

static std::string escape_json(const std::string& js) {
  std::string s(remove_undesired_chars(js));
  std::ostringstream ss;
  for (auto c = s.cbegin(); c != s.cend(); c++) {
    switch (*c) {
      case '"':
        ss << "\\\"";
        break;
      case '\\':
        ss << "\\\\";
        break;
      case '\b':
        ss << "\\b";
        break;
      case '\f':
        ss << "\\f";
        break;
      case '\n':
        ss << "\\n";
        break;
      case '\r':
        ss << "\\r";
        break;
      case '\t':
        ss << "\\t";
        break;
      default:
        if ('\x00' <= *c && *c <= '\x1f') {
          ss << "\\u" << std::hex << std::setw(4) << std::setfill('0')
             << (int)*c;
        } else {
          ss << *c;
        }
    }
  }
  return ss.str();
}

std::string config_to_json(const Config& config) {
  std::stringstream ss;
  ss << "{"
     << "\n  \"http_port\": " << config.get_http_port()
     << ",\n  \"rtsp_port\": " << config.get_rtsp_port()
     << ",\n  \"http_base_dir\": \"" << config.get_http_base_dir() << "\""
     << ",\n  \"log_severity\": " << config.get_log_severity()
     << ",\n  \"playout_delay\": " << config.get_playout_delay()
     << ",\n  \"tic_frame_size_at_1fs\": " << config.get_tic_frame_size_at_1fs()
     << ",\n  \"max_tic_frame_size\": " << config.get_max_tic_frame_size()
     << ",\n  \"sample_rate\": " << config.get_sample_rate()
     << ",\n  \"rtp_mcast_base\": \""
     << escape_json(config.get_rtp_mcast_base()) << "\""
     << ",\n  \"rtp_port\": " << config.get_rtp_port()
     << ",\n  \"ptp_domain\": " << unsigned(config.get_ptp_domain())
     << ",\n  \"ptp_dscp\": " << unsigned(config.get_ptp_dscp())
     << ",\n  \"sap_mcast_addr\": \""
     << escape_json(config.get_sap_mcast_addr()) << "\""
     << ",\n  \"sap_interval\": " << config.get_sap_interval()
     << ",\n  \"syslog_proto\": \"" << escape_json(config.get_syslog_proto())
     << "\""
     << ",\n  \"syslog_server\": \"" << escape_json(config.get_syslog_server())
     << "\""
     << ",\n  \"status_file\": \"" << escape_json(config.get_status_file())
     << "\""
     << ",\n  \"interface_name\": \""
     << escape_json(config.get_interface_name()) << "\""
     << ",\n  \"mdns_enabled\": " << std::boolalpha << config.get_mdns_enabled()
     << ",\n  \"mac_addr\": \"" << escape_json(config.get_mac_addr_str())
     << "\""
     << ",\n  \"ip_addr\": \"" << escape_json(config.get_ip_addr_str()) << "\""
     << ",\n  \"node_id\": \"" << escape_json(get_node_id(config.get_ip_addr()))
     << "\""
     << "\n}\n";
  return ss.str();
}

std::string source_to_json(const StreamSource& source) {
  std::stringstream ss;
  ss << "\n  {"
     << "\n    \"id\": " << unsigned(source.id)
     << ",\n    \"enabled\": " << std::boolalpha << source.enabled
     << ",\n    \"name\": \"" << escape_json(source.name) << "\""
     << ",\n    \"io\": \"" << escape_json(source.io) << "\""
     << ",\n    \"max_samples_per_packet\": " << source.max_samples_per_packet
     << ",\n    \"codec\": \"" << escape_json(source.codec) << "\""
     << ",\n    \"ttl\": " << unsigned(source.ttl)
     << ",\n    \"payload_type\": " << unsigned(source.payload_type)
     << ",\n    \"dscp\": " << +unsigned(source.dscp)
     << ",\n    \"refclk_ptp_traceable\": " << std::boolalpha
     << source.refclk_ptp_traceable << ",\n    \"map\": [ ";
  int i = 0;
  for (int value : source.map) {
    if (i++ > 0)
      ss << ", ";
    ss << value;
  }
  ss << " ]\n  }";
  return ss.str();
}

std::string sink_to_json(const StreamSink& sink) {
  std::stringstream ss;
  ss << "\n  {"
     << "\n    \"id\": " << unsigned(sink.id) << ",\n    \"name\": \""
     << escape_json(sink.name) << "\""
     << ",\n    \"io\": \"" << escape_json(sink.io) << "\""
     << ",\n    \"use_sdp\": " << std::boolalpha << sink.use_sdp
     << ",\n    \"source\": \"" << escape_json(sink.source) << "\""
     << ",\n    \"sdp\": \"" << escape_json(sink.sdp) << "\""
     << ",\n    \"delay\": " << sink.delay
     << ",\n    \"ignore_refclk_gmid\": " << std::boolalpha
     << sink.ignore_refclk_gmid << ",\n    \"map\": [ ";
  int i = 0;
  for (int value : sink.map) {
    if (i++ > 0)
      ss << ", ";
    ss << value;
  }
  ss << " ]\n  }";
  return ss.str();
}

std::string sink_status_to_json(const SinkStreamStatus& status) {
  std::stringstream ss;
  ss << "{";
  ss << "   \n  \"sink_flags\":\n  {" << std::boolalpha
     << " \n    \"rtp_seq_id_error\": " << status.is_rtp_seq_id_error
     << ", \n    \"rtp_ssrc_error\": " << status.is_rtp_ssrc_error
     << ", \n    \"rtp_payload_type_error\": "
     << status.is_rtp_payload_type_error
     << ", \n    \"rtp_sac_error\": " << status.is_rtp_sac_error
     << ", \n    \"receiving_rtp_packet\": " << status.is_receiving_rtp_packet
     << ", \n    \"some_muted\": " << status.is_some_muted
     << ", \n    \"all_muted\": " << status.is_all_muted
     << ", \n    \"muted\": " << status.is_muted << "\n  },"
     << "\n  \"sink_min_time\": " << status.min_time << "\n}\n";
  return ss.str();
}

std::string ptp_config_to_json(const PTPConfig& ptp_config) {
  std::stringstream ss;
  ss << "{"
     << " \"domain\": " << unsigned(ptp_config.domain)
     << ", \"dscp\": " << unsigned(ptp_config.dscp) << " }\n";
  return ss.str();
}

std::string ptp_status_to_json(const PTPStatus& status) {
  std::stringstream ss;
  ss << "{"
     << " \"status\": \"" << escape_json(status.status) << "\""
     << ", \"gmid\": \"" << escape_json(status.gmid) << "\""
     << ", \"jitter\": " << status.jitter << " }\n";
  return ss.str();
}

std::string sources_to_json(const std::list<StreamSource>& sources) {
  int count = 0;
  std::stringstream ss;
  ss << "{\n  \"sources\": [";
  for (auto const& source : sources) {
    if (count++) {
      ss << ", ";
    }
    ss << source_to_json(source);
  }
  ss << "  ]\n}\n";
  return ss.str();
}

std::string sinks_to_json(const std::list<StreamSink>& sinks) {
  int count = 0;
  std::stringstream ss;
  ss << "{\n  \"sinks\": [";
  for (auto const& sink : sinks) {
    if (count++) {
      ss << ", ";
    }
    ss << sink_to_json(sink);
  }
  ss << "  ]\n}\n";
  return ss.str();
}

std::string streams_to_json(const std::list<StreamSource>& sources,
                            const std::list<StreamSink>& sinks) {
  int count = 0;
  std::stringstream ss;
  ss << "{\n  \"sources\": [";
  for (auto const& source : sources) {
    if (count++) {
      ss << ", ";
    }
    ss << source_to_json(source);
  }
  count = 0;
  ss << "  ],\n  \"sinks\": [";
  for (auto const& sink : sinks) {
    if (count++) {
      ss << ", ";
    }
    ss << sink_to_json(sink);
  }
  ss << "  ]\n}\n";
  return ss.str();
}

std::string remote_source_to_json(const RemoteSource& source) {
  std::stringstream ss;
  ss << "\n  {"
     << "\n    \"source\": \"" << escape_json(source.source) << "\""
     << ",\n    \"id\": \"" << escape_json(source.id) << "\""
     << ",\n    \"name\": \"" << escape_json(source.name) << "\""
     << ",\n    \"domain\": \"" << escape_json(source.domain) << "\""
     << ",\n    \"address\": \"" << escape_json(source.address) << "\""
     << ",\n    \"sdp\": \"" << escape_json(source.sdp) << "\""
     << ",\n    \"last_seen\": " << unsigned(source.last_seen)
     << ",\n    \"announce_period\": " << unsigned(source.announce_period)
     << " \n  }";
  return ss.str();
}

std::string remote_sources_to_json(const std::list<RemoteSource>& sources) {
  int count = 0;
  std::stringstream ss;
  ss << "{\n  \"remote_sources\": [";
  for (auto const& source : sources) {
    if (count++) {
      ss << ", ";
    }
    ss << remote_source_to_json(source);
  }
  ss << "  ]\n}\n";
  return ss.str();
}

Config json_to_config_(std::istream& js, Config& config) {
  try {
    boost::property_tree::ptree pt;
    boost::property_tree::read_json(js, pt);

    for (auto const& [key, val] : pt) {
      if (key == "http_port") {
        config.set_http_port(val.get_value<int>());
      } else if (key == "rtsp_port") {
        config.set_rtsp_port(val.get_value<int>());
      } else if (key == "http_base_dir") {
        config.set_http_base_dir(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "log_severity") {
        config.set_log_severity(val.get_value<int>());
      } else if (key == "interface_name") {
        config.set_interface_name(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "playout_delay") {
        config.set_playout_delay(val.get_value<uint32_t>());
      } else if (key == "tic_frame_size_at_1fs") {
        config.set_tic_frame_size_at_1fs(val.get_value<uint32_t>());
      } else if (key == "max_tic_frame_size") {
        config.set_max_tic_frame_size(val.get_value<uint32_t>());
      } else if (key == "sample_rate") {
        config.set_sample_rate(val.get_value<uint32_t>());
      } else if (key == "rtp_mcast_base") {
        config.set_rtp_mcast_base(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "rtp_port") {
        config.set_rtp_port(val.get_value<uint16_t>());
      } else if (key == "ptp_domain") {
        config.set_ptp_domain(val.get_value<uint8_t>());
      } else if (key == "ptp_dscp") {
        config.set_ptp_dscp(val.get_value<uint8_t>());
      } else if (key == "sap_mcast_addr") {
        config.set_sap_mcast_addr(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "sap_interval") {
        config.set_sap_interval(val.get_value<uint16_t>());
      } else if (key == "mdns_enabled") {
        config.set_mdns_enabled(val.get_value<bool>());
      } else if (key == "status_file") {
        config.set_status_file(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "syslog_proto") {
        config.set_syslog_proto(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "syslog_server") {
        config.set_syslog_server(
            remove_undesired_chars(val.get_value<std::string>()));
      } else if (key == "mac_addr" || key == "ip_addr" || key == "node_id") {
        /* ignored */
      } else {
        std::cerr << "Warning: unkown configuration option " << key
                  << std::endl;
      }
    }
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  } catch (...) {
    throw std::runtime_error("failed to convert a number");
  }
  return config;
}

Config json_to_config(std::istream& js, const Config& curConfig) {
  Config config(curConfig);
  return json_to_config_(js, config);
}

Config json_to_config(std::istream& js) {
  Config config;
  return json_to_config_(js, config);
}

Config json_to_config(const std::string& json, const Config& curConfig) {
  std::stringstream ss(json);
  return json_to_config(ss, curConfig);
}

Config json_to_config(const std::string& json) {
  std::stringstream ss(json);
  return json_to_config(ss);
}

StreamSource json_to_source(const std::string& id, const std::string& json) {
  /* JSON request
    "enabled": true,
    "name": "ALSA (on ubuntu)_1",
    "io": "Audio Device",
    "map": [ 0, 1, 2, 3, 4, 5, 6, 7 ],
    "max_samples_per_packet": 48,
    "codec": "L24",
    "ttl": 15,
    "payload_type": 98,
    "dscp": 34,
    "refclk_ptp_traceable": false
  */
  StreamSource source;
  try {
    boost::property_tree::ptree pt;
    std::stringstream ss(json);
    boost::property_tree::read_json(ss, pt);

    source.id = std::stoi(id);
    source.enabled = pt.get<bool>("enabled");
    source.name = remove_undesired_chars(pt.get<std::string>("name"));
    source.io = remove_undesired_chars(pt.get<std::string>("io"));
    /* source map determite the association with
       ALSA output channels used to playing */
    BOOST_FOREACH (boost::property_tree::ptree::value_type& v,
                   pt.get_child("map")) {
      source.map.emplace_back(std::stoi(v.second.data()));
    }
    source.max_samples_per_packet = pt.get<uint32_t>("max_samples_per_packet");
    source.codec = remove_undesired_chars(pt.get<std::string>("codec"));
    source.ttl = pt.get<uint8_t>("ttl");
    source.payload_type = pt.get<uint8_t>("payload_type");
    source.dscp = pt.get<uint8_t>("dscp");
    source.refclk_ptp_traceable = pt.get<bool>("refclk_ptp_traceable");
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  } catch (...) {
    throw std::runtime_error("failed to convert a number");
  }
  return source;
}

StreamSink json_to_sink(const std::string& id, const std::string& json) {
  /* JSON request
    "name": "ALSA (on ubuntu)_1",
    "io": "Audio Device",
    "use_sdp": true,
    "source": "...",
    "sdp": "...",
    "delay": 384,
    "ignore_refclk_gmid": false,
    "map": [ 0, 1, 2, 3, 4, 5, 6, 7 ]
  */
  StreamSink sink;
  try {
    boost::property_tree::ptree pt;
    std::stringstream ss(json);
    boost::property_tree::read_json(ss, pt);

    sink.id = std::stoi(id);
    sink.name = remove_undesired_chars(pt.get<std::string>("name"));
    sink.io = remove_undesired_chars(pt.get<std::string>("io"));
    sink.source = remove_undesired_chars(pt.get<std::string>("source"));
    sink.use_sdp = pt.get<bool>("use_sdp");
    sink.sdp = remove_undesired_chars(pt.get<std::string>("sdp"));
    sink.delay = pt.get<uint32_t>("delay");
    sink.ignore_refclk_gmid = pt.get<bool>("ignore_refclk_gmid");
    /* source map determite the association with
       ALSA input channels used to recording */
    BOOST_FOREACH (boost::property_tree::ptree::value_type& v,
                   pt.get_child("map")) {
      sink.map.emplace_back(std::stoi(v.second.data()));
    }
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  } catch (...) {
    throw std::runtime_error("failed to convert a number");
  }
  return sink;
}

PTPConfig json_to_ptp_config(const std::string& json) {
  PTPConfig ptpConfig;
  try {
    boost::property_tree::ptree pt;
    std::stringstream ss(json);
    boost::property_tree::read_json(ss, pt);

    ptpConfig.domain = pt.get<int>("domain");
    ptpConfig.dscp = pt.get<int>("dscp");
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  }
  return ptpConfig;
}

void json_to_sources(const std::string& json,
                     std::list<StreamSource>& sources) {
  std::stringstream ss(json);
  return json_to_sources(ss, sources);
}

static void parse_json_sources(boost::property_tree::ptree& pt,
                               std::list<StreamSource>& sources) {
  BOOST_FOREACH (auto const& v, pt.get_child("sources")) {
    StreamSource source;
    source.id = v.second.get<uint8_t>("id");
    source.enabled = v.second.get<bool>("enabled");
    source.name = v.second.get<std::string>("name");
    source.io = v.second.get<std::string>("io");
    source.max_samples_per_packet =
        v.second.get<uint32_t>("max_samples_per_packet");
    source.codec = v.second.get<std::string>("codec");
    source.ttl = v.second.get<uint8_t>("ttl");
    source.payload_type = v.second.get<uint8_t>("payload_type");
    source.dscp = v.second.get<uint8_t>("dscp");
    source.refclk_ptp_traceable = v.second.get<bool>("refclk_ptp_traceable");
    /* source map determite the association with
       ALSA output channels used to playing */
    BOOST_FOREACH (const boost::property_tree::ptree::value_type& vm,
                   v.second.get_child("map")) {
      source.map.emplace_back(std::stoi(vm.second.data()));
    }
    sources.emplace_back(std::move(source));
  }
}

void json_to_sources(std::istream& js, std::list<StreamSource>& sources) {
  try {
    boost::property_tree::ptree pt;
    boost::property_tree::read_json(js, pt);
    parse_json_sources(pt, sources);
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  }
}

void json_to_sinks(const std::string& json, std::list<StreamSink>& sinks) {
  std::stringstream ss(json);
  return json_to_sinks(ss, sinks);
}

static void parse_json_sinks(boost::property_tree::ptree& pt,
                             std::list<StreamSink>& sinks) {
  BOOST_FOREACH (auto const& v, pt.get_child("sinks")) {
    StreamSink sink;
    sink.id = v.second.get<uint8_t>("id");
    sink.name = v.second.get<std::string>("name");
    sink.io = v.second.get<std::string>("io");
    sink.source = v.second.get<std::string>("source");
    sink.use_sdp = v.second.get<bool>("use_sdp");
    sink.sdp = v.second.get<std::string>("sdp");
    sink.delay = v.second.get<uint32_t>("delay");
    sink.ignore_refclk_gmid = v.second.get<bool>("ignore_refclk_gmid");
    /* source map determite the association with
       ALSA input channels used to recording */
    BOOST_FOREACH (const boost::property_tree::ptree::value_type& vm,
                   v.second.get_child("map")) {
      sink.map.emplace_back(std::stoi(vm.second.data()));
    }
    sinks.emplace_back(std::move(sink));
  }
}

void json_to_sinks(std::istream& js, std::list<StreamSink>& sinks) {
  try {
    boost::property_tree::ptree pt;
    boost::property_tree::read_json(js, pt);
    parse_json_sinks(pt, sinks);
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  }
}

void json_to_streams(const std::string& json,
                     std::list<StreamSource>& sources,
                     std::list<StreamSink>& sinks) {
  std::stringstream ss(json);
  json_to_streams(ss, sources, sinks);
}

void json_to_streams(std::istream& js,
                     std::list<StreamSource>& sources,
                     std::list<StreamSink>& sinks) {
  try {
    boost::property_tree::ptree pt;
    boost::property_tree::read_json(js, pt);
    parse_json_sources(pt, sources);
    parse_json_sinks(pt, sinks);
  } catch (boost::property_tree::json_parser::json_parser_error& je) {
    throw std::runtime_error("error parsing JSON at line " +
                             std::to_string(je.line()) + " :" + je.message());
  }
}
