/*
 * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The OpenAirInterface Software Alliance licenses this file to You under
 * the OAI Public License, Version 1.1  (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.openairinterface.org/?page_id=698
 *
 * 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.
 *-------------------------------------------------------------------------------
 * For more information about the OpenAirInterface (OAI) Software Alliance:
 *      contact@openairinterface.org
 */

/*! \file common/utils/websrv/frontend/src/app/components/scope/scope.component.ts
 * \brief: implementation of web interface frontend for oai
 * \scope component web interface implementation (works with scope.component.html)
 * \author:  Yacine  El Mghazli, Francois TABURET
 * \date 2022
 * \version 0.1
 * \company NOKIA BellLabs France
 * \email: yacine.el_mghazli@nokia-bell-labs.com  francois.taburet@nokia-bell-labs.com
 * \note
 * \warning
 */
import {Component, EventEmitter, OnDestroy, OnInit, Output, QueryList, ViewChildren} from "@angular/core";
import {Chart, ChartConfiguration} from "chart.js";
import {BaseChartDirective} from "ng2-charts";
import {Subscription} from "rxjs";
import {HelpApi} from "src/app/api/help.api";
import {IGraphDesc, IScopeDesc, IScopeGraphType, ISigDesc, ScopeApi} from "src/app/api/scope.api";
import {arraybuf_data_offset, Message, WebSocketService, webSockSrc} from "src/app/services/websocket.service";

export interface RxScopeMessage {
  msgtype: number;
  chartid: number;
  dataid: number;
  segnum: number;
  update: boolean;
  content: ArrayBuffer;
}

const deserialize = (fullbuff: ArrayBuffer):
    RxScopeMessage => {
      const header = new DataView(fullbuff, 0, arraybuf_data_offset);
      return {
        // source: src.getUint8(0),  //header
        msgtype : header.getUint8(1), // header
        chartid : header.getUint8(3), // header
        dataid : header.getUint8(4), // header
        segnum : header.getUint8(2), // header
        update : (header.getUint8(5) == 1) ? true : false, // header
        content : fullbuff.slice(arraybuf_data_offset) // data
      };
    }

const serialize = (msg: TxScopeMessage):
    ArrayBuffer => {
      const byteArray = new TextEncoder().encode(msg.content);

      let arr = new Uint8Array(byteArray.byteLength + arraybuf_data_offset);
      arr.set(byteArray, arraybuf_data_offset) // data
      let buffview = new DataView(arr.buffer);
      buffview.setUint8(1, msg.msgtype); // header

      return buffview.buffer;
    }

export interface TxScopeMessage {
  msgtype: number;
  content: string;
}

/*------------------------------------*/
/* constants that must match backend (websrv.h or phy_scope.h) */
const SCOPEMSG_TYPE_TIME = 3;
const SCOPEMSG_TYPE_DATA = 10;
const SCOPEMSG_TYPE_DATAACK = 11;
const SCOPEMSG_TYPE_DATAFLOW = 12;

const SCOPEMSG_DATA_IQ = 1;
const SCOPEMSG_DATA_LLR = 2;
const SCOPEMSG_DATA_WF = 3;
const SCOPEMSG_DATA_TRESP = 4;
/*---------------------------------------*/
@Component({
  selector : "app-scope",
  templateUrl : "./scope.component.html",
  styleUrls : [ "./scope.component.css" ],
})

export class ScopeComponent implements OnInit, OnDestroy {
  // data for scope status area
  scopetitle = "";
  scopesubtitle = "";
  scopetime = "";
  scopestatus = "stopped";
  skippedmsg = "0";
  bufferedmsg = "0";
  // data for scope control area
  startstop = "start";
  startstop_color = "warn";
  rfrate = 2;
  data_ACK = false;
  // data for Time Response chart
  TRespgraph: IGraphDesc = {title : "", type: IScopeGraphType.TRESP, id: -1, srvidx: -1};
  enable_TResp = false;
  // data for scope iq constellation area
  iqgraph_list: IGraphDesc[] = [];
  selected_channels = [ "" ];
  iqmax = 32767;
  iqmin = -32767;
  iqxmin = this.iqmin;
  iqymin = this.iqmin;
  iqxmax = this.iqmax;
  iqymax = this.iqmax;
  // data for scope LLR area
  llrgraph_list: IGraphDesc[] = [];
  selected_llrchannels = [ "" ];
  sig_list: ISigDesc[] = [
    {target_id : 0, antenna_id: 0},
    {target_id : 1, antenna_id: 0},
    {target_id : 2, antenna_id: 0},
    {target_id : 3, antenna_id: 0},
  ];
  selected_sig: ISigDesc = {target_id : 0, antenna_id: 0};
  llrythresh = 5;
  llrmin = 0;
  llrmax = 200000;
  llrxmin = this.llrmin;
  llrxmax = this.llrmax;
  // data for scope WatterFall area
  WFgraph_list: IGraphDesc[] = [];
  selected_WF = "";
  llrchart?: Chart;
  iqcchart?: Chart;
  wfchart?: Chart;
  trespchart?: Chart;
  nwf: number[] = [ 0, 0, 0, 0 ];

  // websocket service object and related subscription for message reception
  wsSubscription?: Subscription;

  @Output() ScopeEnabled = new EventEmitter<boolean>();

  @ViewChildren(BaseChartDirective) charts?: QueryList<BaseChartDirective>;

  public TRespDatasets: ChartConfiguration<"line">[ "data" ]["datasets"] = [
    {
      data : [],
      label: "",
      pointRadius: 1,
      showLine: true,
      animation: false,
      fill: false,
      pointStyle: "circle",
      backgroundColor: "rgba(255,0,0,0)",
      borderColor: "red",
      pointBorderColor: "red",
    },
  ];
  public IQDatasets: ChartConfiguration<"scatter">[ "data" ]["datasets"] = [
    {
      data : [],
      label: "C1",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      fill: false,
      pointStyle: "circle",
      //      pointBackgroundColor: 'yellow',
      backgroundColor: "yellow",
      borderWidth: 0,
      pointBorderColor: "yellow",
      //    parsing: false,
    },
    {
      data : [],
      label: "C2",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "cyan",
      backgroundColor: "cyan",
      borderWidth: 0,
      pointBorderColor: "cyan",
      //      parsing: false,
    },
    {
      data : [],
      label: "C3",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "red",
      backgroundColor: "red",
      borderWidth: 0,
      pointBorderColor: "red",
      //      parsing: false,
    }
  ];

  public LLRDatasets: ChartConfiguration<"scatter">[ "data" ]["datasets"] = [
    {
      data : [],
      label: "LLR1",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      fill: false,
      pointStyle: "circle",
      pointBackgroundColor: "yellow",
      backgroundColor: "yellow",
      borderWidth: 0,
      pointBorderColor: "yellow",
      //     parsing: false,
    },
    {
      data : [],
      label: "LL2",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "cyan",
      backgroundColor: "cyan",
      borderWidth: 0,
      pointBorderColor: "cyan",
      parsing: false,
    },
    {
      data : [],
      label: "LLR3",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "red",
      backgroundColor: "red",
      borderWidth: 0,
      pointBorderColor: "red",
      parsing: false,
    }
  ];

  public WFDatasets: ChartConfiguration<"scatter">[ "data" ]["datasets"] = [
    {
      data : [],
      label: "WFblue",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      fill: false,
      pointStyle: "circle",
      pointBackgroundColor: "blue",
      backgroundColor: "blue",
      borderWidth: 0,
      pointBorderColor: "blue",
      //     parsing: false,
    },
    {
      data : [],
      label: "WFgreen",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "green",
      backgroundColor: "green",
      borderWidth: 0,
      pointBorderColor: "green",
      //      parsing: false,
    },
    {
      data : [],
      label: "WFyellow",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "yellow",
      backgroundColor: "yellow",
      borderWidth: 0,
      pointBorderColor: "yellow",
      //      parsing: false,
    },
    {
      data : [],
      label: "WFred",
      pointRadius: 0.5,
      showLine: false,
      animation: false,
      pointStyle: "circle",
      pointBackgroundColor: "red",
      backgroundColor: "red",
      borderWidth: 0,
      pointBorderColor: "red",
      //     parsing: false,
    }
  ];
  //  help text from backend
  help_ack: string = "";

  public TRespOptions: ChartConfiguration<"line">[ "options" ] = {
    responsive : true,
    //    aspectRatio: 2,
    plugins: {
      legend: {display: true, labels: {boxWidth: 10, boxHeight: 10}},
      tooltip: {
        enabled: false,
      },
    },
  };
  TRespLabels: number[] = [ 0 ];
  public IQOptions: ChartConfiguration<"scatter">[ "options" ] = {
    responsive : true,
    aspectRatio: 1,
    //    scales: {
    //      xAxes: {
    //		  min: -5,
    //		  max:5,
    //      }
    //    },
    plugins: {
      legend: {display: true, labels: {boxWidth: 10, boxHeight: 10}},
      tooltip: {
        enabled: false,
      },
    },
  };

  public LLROptions: ChartConfiguration<"scatter">[ "options" ] = {
    responsive : true,
    aspectRatio: 3,
    scales: {
      xAxes: {
        min: 0,
      },
      //      yAxes: {
      //		  min: -100,
      //		  max: 100
      //      }
    },
    plugins: {
      legend: {display: true, labels: {boxWidth: 10, boxHeight: 10}},
      tooltip: {
        enabled: false,
      },
    },
  };

  public WFOptions: ChartConfiguration<"scatter">[ "options" ] = {
    responsive : true,
    aspectRatio: 5,
    scales: {
      xAxes: {
        min: 0,
        //		  max:800,
      },
      yAxes: {
        min: 0,
        max: 80,
        reverse: true,
      }
    },
    plugins: {
      legend: {display: true, labels: {boxWidth: 10, boxHeight: 10}},
      tooltip: {
        enabled: false,
      },
    },
  }

  constructor(private scopeApi: ScopeApi, private wsService: WebSocketService, public helpApi: HelpApi)
  {
    console.log("Scope constructor ");
  }

  ngOnInit()
  {
    console.log("Scope ngOnInit ");
    this.scopeApi.getScopeInfos$().subscribe(resp => { this.configScope(resp); });

    this.helpApi.getHelpText("scope", "control", "dataack").subscribe(resp => { this.help_ack = resp; }, err => { this.help_ack = ""; });
  }

  ngOnDestroy()
  {
    console.log("Scope ngOnDestroy ");
    this.wsSubscription?.unsubscribe();
  }

  DecodScopeBinmsgToString(message: ArrayBuffer): string
  {
    const enc = new TextDecoder("utf-8");
    return enc.decode(message);
  }

  private configScope(resp: IScopeDesc)
  {
    if (resp.title === "none") {
      this.ScopeEnabled.emit(false);
    } else {
      this.ScopeEnabled.emit(true);
      this.scopetitle = resp.title;
      this.iqgraph_list.length = 0;
      this.llrgraph_list.length = 0;
      for (let graphIndex = 0; graphIndex < resp.graphs.length; graphIndex++) {
        if (resp.graphs[graphIndex].type == IScopeGraphType.IQs) {
          this.iqgraph_list.push(resp.graphs[graphIndex]);
          this.IQDatasets[this.iqgraph_list.length - 1].label = resp.graphs[graphIndex].title;
        }
        if (resp.graphs[graphIndex].type == IScopeGraphType.LLR) {
          this.llrgraph_list.push(resp.graphs[graphIndex]);
          this.LLRDatasets[this.llrgraph_list.length - 1].label = resp.graphs[graphIndex].title;
        }
        if (resp.graphs[graphIndex].type == IScopeGraphType.WF) {
          this.WFgraph_list.push(resp.graphs[graphIndex]);
        }
        if (resp.graphs[graphIndex].type == IScopeGraphType.TRESP) {
          this.TRespgraph = resp.graphs[graphIndex];
          this.TRespDatasets[0].label = resp.graphs[graphIndex].title;
        }
      }
      this.charts?.forEach((child) => {child.chart?.update()});
    }
  }

  private ProcessScopeMsg(message: RxScopeMessage)
  {
    if (this.scopestatus === "starting") {
      this.scopestatus = "started";
      this.startstop = "stop";
      this.startstop_color = "started";
    }
    let d = 0;
    let x = 0;
    let y = 0;
    switch (message.msgtype) {
      case SCOPEMSG_TYPE_TIME:
        this.scopetime = this.DecodScopeBinmsgToString(message.content);
        break;
      case SCOPEMSG_TYPE_DATAFLOW:
        let infobuff = this.DecodScopeBinmsgToString(message.content).split("|");
        if (infobuff.length >= 2) {
          this.skippedmsg = infobuff[0]!;
          this.bufferedmsg = infobuff[1]!;
        }
        break;
      case SCOPEMSG_TYPE_DATA:
        const bufferview = new DataView(message.content);
        if (message.update) {
          console.log("Starting scope update chart " + message.chartid.toString() + ", dataset " + message.dataid.toString() + " data length: " + bufferview.byteLength);
        }
        switch (message.chartid) {
          case SCOPEMSG_DATA_TRESP:
            d = 0;
            for (let i = 0; i < bufferview.byteLength; i = i + 4) {
              this.TRespDatasets[0].data[d] = {x : d, y : bufferview.getFloat32(i, true)};
              this.TRespLabels[d] = d;
              d++;
            }
            if (message.update) {
              this.trespchart!.update();
              console.log(" TRESP view update completed " + d.toString() + " points ");
            }
            break;
          case SCOPEMSG_DATA_IQ:
            this.IQDatasets[message.dataid].data.length = 0;
            for (let i = 0; i < bufferview.byteLength; i = i + 4) {
              this.IQDatasets[message.dataid].data[i / 4] = {x : bufferview.getInt16(i, true), y : bufferview.getInt16(i + 2, true)};
            }

            if (message.update) {
              this.iqcchart!.update();
            }
            break;
          case SCOPEMSG_DATA_LLR:
            this.LLRDatasets[message.dataid].data.length = 0;
            let xoffset = 0;
            d = 0;
            for (let i = 4; i < (bufferview.byteLength - 2); i = i + 4) {
              xoffset = xoffset + bufferview.getInt16(i + 2, true);
              this.LLRDatasets[message.dataid].data[d] = {x : xoffset, y : bufferview.getInt16(i, true)};
              d++;
            }
            this.LLRDatasets[message.dataid].data[d] = {x : bufferview.getInt32(0, true), y : 0};
            if (message.update) {
              this.llrchart!.update();
            }

            break;
          case SCOPEMSG_DATA_WF:
            if (message.update) {
              if (message.segnum == 0) {
                for (let i = 0; i < this.WFDatasets.length; i++) {
                  this.nwf[i] = 0;
                  this.WFDatasets[i].data.length = 0;
                }
              }
            }
            for (let i = 2; i < bufferview.byteLength - 2; i = i + 4) { // first 16 bits in buffer contains the number of points in the message
              x = bufferview.getInt16(i, true);
              y = bufferview.getInt16(i + 2, true);
              this.wfchart!.scales.xAxes.max = bufferview.getInt16(bufferview.byteLength - 4, true);
              this.WFDatasets[message.dataid].data[this.nwf[message.dataid]] = {x : x, y : y};
              this.nwf[message.dataid]++;
            }
            if (message.update) {
              this.wfchart!.update();
              console.log(" WF view update completed " + d.toString() + "points, ");
            }
            break;
          default:
            break;
        }

        this.sendMsg(SCOPEMSG_TYPE_DATAACK, "Chart " + message.chartid.toString() + ", dataset " + message.dataid.toString());
        break;
      default:
        break;
    }
  }

  private sendMsg(type: number, strmessage: string)
  {
    this.wsService.send({source : webSockSrc.softscope, fullbuff : serialize({msgtype : type, content : strmessage})});
    console.log("Scope sent msg type " + type.toString() + " " + strmessage);
  }

  private SendScopeParams(name: string, value: string, graphid: number): number
  {
    let status = 0;
    this.scopeApi.setScopeParams$({name : name, value : value, graphid : graphid})
        .subscribe(response => { console.log(response.status); },
                   err => {
                     console.log("scope SendScopeParams: error received: " + err);
                     this.StopScope();
                   },
                   () => console.log("scope SendScopeParams OK"));
    return status;
  }

  private StopScope()
  {
    if (this.wsSubscription)
      this.wsSubscription.unsubscribe();

    this.scopestatus = "stopped";
    this.startstop = "start";
    this.startstop_color = "warn";
    this.skippedmsg = "0";
    this.bufferedmsg = "0";
  }

  startorstop()
  {
    if (this.scopestatus === "stopped") {
      let status = this.SendScopeParams("startstop", "start", 0);
      if (status == 0) {
        this.llrchart = Chart.getChart("llr");
        this.iqcchart = Chart.getChart("iqc");
        this.wfchart = Chart.getChart("wf");
        this.trespchart = Chart.getChart("tresp");
        this.IQDatasets.forEach((dataset) => {dataset.data.length = 0});
        this.LLRDatasets.forEach((dataset) => {dataset.data.length = 0});
        this.WFDatasets.forEach((dataset) => {dataset.data.length = 0});
        this.TRespDatasets.forEach((dataset) => {dataset.data.length = 0});
        this.charts?.forEach((child, index) => {child.chart?.update()});
        this.scopestatus = "starting";
        this.SigChanged(this.selected_sig.target_id);
        this.OnRefrateChange();
        this.OnIQxminChange();
        this.OnIQxmaxChange();
        this.OnIQyminChange();
        this.OnIQymaxChange();
        this.OnYthreshChange();
        this.OnLLRxminChange();
        this.OnLLRxmaxChange();
        this.channelsChanged(this.selected_channels);
        this.llrchannelsChanged(this.selected_llrchannels);
        this.WFChanged(this.selected_WF);
        this.onEnableTResp();
        for (let i = 0; i < this.WFDatasets.length; i++) {
          this.nwf[i] = 0;
          this.WFDatasets[i].data.length = 0;
        }
        this.wsService = new (WebSocketService);

        this.wsSubscription = this.wsService.subject$.subscribe(msg => this.ProcessScopeMsg(deserialize(msg.fullbuff)),
                                                                err => {
                                                                  console.error("WebSocket Observer got an error: " + err);
                                                                  this.StopScope()
                                                                },
                                                                () => { console.error("WebSocket Observer completed: "); })
      }
    } else {
      this.SendScopeParams("startstop", "stop", 0);
      this.StopScope();
    }
  }

  OnRefrateChange()
  {
    this.SendScopeParams("refrate", (this.rfrate * 10).toString(), 0);
  }

  OnLLRxminChange()
  {
    this.SendScopeParams("llrxmin", (this.llrxmin).toString(), 0);
  }
  OnLLRxmaxChange()
  {
    this.SendScopeParams("llrxmax", (this.llrxmax).toString(), 0);
  }

  OnIQxminChange()
  {
    this.SendScopeParams("xmin", (this.iqxmin).toString(), 0);
  }
  OnIQxmaxChange()
  {
    this.SendScopeParams("xmax", (this.iqxmax).toString(), 0);
  }
  OnIQyminChange()
  {
    this.SendScopeParams("ymin", (this.iqymin).toString(), 0);
  }
  OnIQymaxChange()
  {
    this.SendScopeParams("ymax", (this.iqymax).toString(), 0);
  }

  channelsChanged(titles: string[])
  {
    this.selected_channels = titles;
    this.iqgraph_list.forEach(graph => {
      const [enabled] = titles.filter(value => graph.title === value);
      this.SendScopeParams("enabled", enabled ? "true" : "false", graph.srvidx);
    })
  }

  llrchannelsChanged(titles: string[])
  {
    this.selected_llrchannels = titles;
    this.llrgraph_list.forEach(graph => {
      const [enabled] = titles.filter(value => graph.title === value);
      this.SendScopeParams("enabled", enabled ? "true" : "false", graph.srvidx);
    })
  }

  SigChanged(value: number)
  {
    this.selected_sig.target_id = value;
    if (this.scopetitle === "gNB")
      this.scopesubtitle = " - sig from UE" + value.toString() + " antenna" + this.selected_sig.antenna_id;
    else
      this.scopesubtitle = " - sig from gNB" + value.toString() + " antenna" + this.selected_sig.antenna_id;
    ;
    this.SendScopeParams("TargetSelect", value.toString(), 0);
  }

  WFChanged(value: string)
  {
    this.selected_WF = value;

    for (let i = 0; i < this.WFgraph_list.length; i++) {
      if (this.WFgraph_list[i].title === value) {
        this.SendScopeParams("enabled", "true", this.WFgraph_list[i].srvidx);
        this.WFDatasets[0].label = value;
        this.WFDatasets[1].label = "(>avg*2)";
        this.WFDatasets[2].label = "(>avg*10)";
        this.WFDatasets[3].label = "(>avg*100)";
      } else {
        this.SendScopeParams("enabled", "false", this.WFgraph_list[i].srvidx);
      }
    }
    for (let i = 0; i < this.WFDatasets.length; i++) {
      this.nwf[i] = 0;
      this.WFDatasets[i].data.length = 0;
    }
  }
  onEnableTResp()
  {
    this.SendScopeParams("enabled", this.enable_TResp.toString(), this.TRespgraph.srvidx);
  }

  onDataACKchange()
  {
    this.SendScopeParams("DataAck", this.data_ACK.toString(), 0);
  }

  OnYthreshChange()
  {
    this.SendScopeParams("llrythresh", this.llrythresh.toString(), 0);
  }

  RefreshTResp()
  {
    this.TRespDatasets[0].data.length = 0;
    this.trespchart!.update();
  }

  RefreshIQV()
  {
    this.IQDatasets.forEach((dataset) => {dataset.data.length = 0});
    this.iqxmin = this.iqmin;
    this.iqymin = this.iqmin;
    this.iqxmax = this.iqmax;
    this.iqymax = this.iqmax;
    this.iqcchart!.update();
  }

  RefreshLLR()
  {
    this.LLRDatasets.forEach((dataset) => {dataset.data.length = 0});
    this.llrxmin = this.llrmin;
    this.llrxmax = this.llrmax;
    this.llrchart!.update();
  }

  RefreshWF()
  {
    this.WFDatasets.forEach((dataset) => {dataset.data.length = 0});
    this.nwf = [ 0, 0, 0, 0 ];
    this.wfchart!.update();
  }
}