'use strict';
// ==================================================================================
// graphics.js
// ----------------------------------------------------------------------------------
// Description:   System Information - library
//                for Node.js
// Copyright:     (c) 2014 - 2017
// Author:        Sebastian Hildebrandt
// ----------------------------------------------------------------------------------
// License:       MIT
// ==================================================================================
// 7. Graphics (controller, display)
// ----------------------------------------------------------------------------------

const os = require('os');
const exec = require('child_process').exec;
const fs = require('fs');
const util = require('./util');

let _platform = os.type();

const _linux = (_platform === 'Linux');
const _darwin = (_platform === 'Darwin');
const _windows = (_platform === 'Windows_NT');
const NOT_SUPPORTED = 'not supported';
let _resolutionx = 0;
let _resolutiony = 0;
let _pixeldepth = 0;

function getValue(lines, property, separator) {
  separator = separator || ':';
  property = property.toLowerCase();
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].toLowerCase().startsWith(property)) {
      const parts = lines[i].split(separator);
      if (parts.length > 1) {
        return parts[1].trim();
      }
    }
  }
  return '';
}

function toInt(value) {
  let result = parseInt(value,10);
  if (isNaN(result)) {
    result = 0;
  }
  return result
}

function graphics(callback) {

  function parseLinesDarwin(lines) {
    let starts = [];
    let level = -1;
    let lastlevel = -1;
    let controllers = [];
    let displays = [];
    let currentController = {};
    let currentDisplay = {};
    for (let i = 0; i < lines.length; i++) {
      if ('' !== lines[i].trim()) {
        let start = lines[i].search(/\S|$/);
        if (-1 === starts.indexOf(start)) {
          starts.push(start);
        }
        level = starts.indexOf(start);
        if (level < lastlevel) {
          if (Object.keys(currentController).length > 0) {// just changed to Displays
            controllers.push(currentController);
            currentController = {};
          }
          if (Object.keys(currentDisplay).length > 0) {// just changed to Displays
            displays.push(currentDisplay);
            currentDisplay = {};
          }
        }
        lastlevel = level;
        let parts = lines[i].split(':');
        if (2 === level) {       // grafics controller level
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('chipsetmodel') !== -1) currentController.model = parts[1].trim();
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('bus') !== -1) currentController.bus = parts[1].trim();
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('vendor') !== -1) currentController.vendor = parts[1].split('(')[0].trim();
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('vram(total)') !== -1) {
            currentController.vram = parseInt(parts[1]);    // in MB
            currentController.vramDynamic = false;
          }
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('vram(dynamic,max)') !== -1) {
            currentController.vram = parseInt(parts[1]);    // in MB
            currentController.vramDynamic = true;
          }
        }
        if (3 === level) {       // display controller level
          if (parts.length > 1 && '' === parts[1]) {
            currentDisplay.model = parts[0].trim();
            currentDisplay.main = false;
            currentDisplay.builtin = false;
            currentDisplay.connection = '';
            currentDisplay.sizex = -1;
            currentDisplay.sizey = -1;
          }
        }
        if (4 === level) {       // display controller details level
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('resolution') !== -1) {
            let resolution = parts[1].split('x');
            currentDisplay.resolutionx = (resolution.length > 1 ? parseInt(resolution[0]) : 0);
            currentDisplay.resolutiony = (resolution.length > 1 ? parseInt(resolution[1]) : 0);
          }
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('pixeldepth') !== -1) currentDisplay.pixeldepth = parseInt(parts[1]); // in BIT
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('maindisplay') !== -1 && parts[1].replace(/ +/g, "").toLowerCase() === 'yes') currentDisplay.main = true;
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('built-in') !== -1 && parts[1].replace(/ +/g, "").toLowerCase() === 'yes') {
            currentDisplay.builtin = true;
            currentDisplay.connection = '';
          }
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('connectiontype') !== -1) {
            currentDisplay.builtin = false;
            currentDisplay.connection = parts[1].trim();
          }
        }
      }
    }
    if (Object.keys(currentController).length > 0) {// just changed to Displays
      controllers.push(currentController);
    }
    if (Object.keys(currentDisplay).length > 0) {// just changed to Displays
      displays.push(currentDisplay);
    }
    return ({
      controllers: controllers,
      displays: displays
    })
  }

  function parseLinesLinuxControllers(lines) {
    let controllers = [];
    let currentController = {};
    let is_vga = false;
    for (let i = 0; i < lines.length; i++) {
      if ('' !== lines[i].trim()) {
        if (' ' !== lines[i][0] && '\t' !== lines[i][0]) {        // first line of new entry
          let vgapos = lines[i].toLowerCase().indexOf('vga');
          if (vgapos !== -1) {         // VGA
            if (Object.keys(currentController).length > 0) {// already a controller found
              controllers.push(currentController);
              currentController = {};
            }
            is_vga = true;
            let endpos = lines[i].search(/\[[0-9a-f]{4}:[0-9a-f]{4}]|$/);
            let parts = lines[i].substr(vgapos, endpos - vgapos).split(':');
            if (parts.length > 1) {
              parts[1] = parts[1].trim();
              if (parts[1].toLowerCase().indexOf('corporation')) {
                currentController.vendor = parts[1].substr(0, parts[1].toLowerCase().indexOf('corporation') + 11).trim();
                currentController.model = parts[1].substr(parts[1].toLowerCase().indexOf('corporation') + 11, 200).trim().split('(')[0];
                currentController.bus = '';
                currentController.vram = -1;
                currentController.vramDynamic = false;
              }
            }

          } else {
            is_vga = false;
          }
        }
        if (is_vga) { // within VGA details
          let parts = lines[i].split(':');
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('devicename') !== -1 && parts[0].toLowerCase().indexOf('onboard') !== -1)  currentController.bus = 'Onboard';
          if (parts.length > 1 && parts[0].replace(/ +/g, "").toLowerCase().indexOf('region') !== -1 && parts[1].toLowerCase().indexOf('memory') !== -1) {
            let memparts = parts[1].split("=");
            if (memparts.length > 1) {
              currentController.vram = parseInt(memparts[1]);
            }
          }
        }
      }
    }
    if (Object.keys(currentController).length > 0) {// still controller information available
      controllers.push(currentController);
    }
    return (controllers)
  }

  function parseLinesLinuxEdid(edid) {
    // parsen EDID
    // --> model
    // --> resolutionx
    // --> resolutiony
    // --> builtin = false
    // --> pixeldepth (?)
    // --> sizex
    // --> sizey
    let result = {};
    // find first "Detailed Timing Description"
    let start = 108;
    if (edid.substr(start, 6) === '000000') {
      start += 36;
    }
    if (edid.substr(start, 6) === '000000') {
      start += 36;
    }
    if (edid.substr(start, 6) === '000000') {
      start += 36;
    }
    if (edid.substr(start, 6) === '000000') {
      start += 36;
    }
    result.resolutionx = parseInt('0x0' + edid.substr(start + 8, 1) + edid.substr(start + 4, 2));
    result.resolutiony = parseInt('0x0' + edid.substr(start + 14, 1) + edid.substr(start + 10, 2));
    result.sizex = parseInt('0x0' + edid.substr(start + 28, 1) + edid.substr(start + 24, 2));
    result.sizey = parseInt('0x0' + edid.substr(start + 29, 1) + edid.substr(start + 26, 2));
    // monitor name
    start = edid.indexOf('000000fc00'); // find first "Monitor Description Data"
    if (start >= 0) {
      let model_raw = edid.substr(start + 10, 26);
      if (model_raw.indexOf('0a') !== -1) {
        model_raw = model_raw.substr(0, model_raw.indexOf('0a'))
      }
      result.model = model_raw.match(/.{1,2}/g).map(function (v) {
        return String.fromCharCode(parseInt(v, 16));
      }).join('');
    } else {
      result.model = '';
    }
    return result;
  }

  function parseLinesLinuxDisplays(lines, depth) {
    let displays = [];
    let currentDisplay = {};
    let is_edid = false;
    let edid_raw = '';
    let start = 0;
    for (let i = 1; i < lines.length; i++) {        // start with second line
      if ('' !== lines[i].trim()) {
        if (' ' !== lines[i][0] && '\t' !== lines[i][0] && lines[i].toLowerCase().indexOf(' connected ') !== -1) {        // first line of new entry
          if (Object.keys(currentDisplay).length > 0) {         // push last display to array
            displays.push(currentDisplay);
            currentDisplay = {};
          }
          let parts = lines[i].split(' ');
          currentDisplay.connection = parts[0];
          currentDisplay.main = (parts[2] === 'primary');
          currentDisplay.builtin = (parts[0].toLowerCase().indexOf('edp') >= 0)
        }

        // try to read EDID information
        if (is_edid) {
          if (lines[i].search(/\S|$/) > start) {
            edid_raw += lines[i].toLowerCase().trim();
          } else {
            // parsen EDID
            let edid_decoded = parseLinesLinuxEdid(edid_raw);
            currentDisplay.model = edid_decoded.model;
            currentDisplay.resolutionx = edid_decoded.resolutionx;
            currentDisplay.resolutiony = edid_decoded.resolutiony;
            currentDisplay.sizex = edid_decoded.sizex;
            currentDisplay.sizey = edid_decoded.sizey;
            currentDisplay.pixeldepth = depth;
            is_edid = false;
          }
        }
        if (lines[i].toLowerCase().indexOf('edid:') !== -1) {
          is_edid = true;
          start = lines[i].search(/\S|$/);
        }
      }
    }

    // pushen displays
    if (Object.keys(currentDisplay).length > 0) {         // still information there
      displays.push(currentDisplay);
    }
    return displays
  }

  // function starts here
  return new Promise((resolve, reject) => {
    process.nextTick(() => {
      let result = {
        controllers: [],
        displays: []
      };
      if (_darwin) {
        let cmd = 'system_profiler SPDisplaysDataType';
        exec(cmd, function (error, stdout) {
          if (!error) {
            let lines = stdout.toString().split('\n');
            result = parseLinesDarwin(lines);
          }
          if (callback) {
            callback(result)
          }
          resolve(result);
        })
      }
      if (_linux) {
        let cmd = 'lspci -vvv  2>/dev/null';
        exec(cmd, function (error, stdout) {
          if (!error) {
            let lines = stdout.toString().split('\n');
            result.controllers = parseLinesLinuxControllers(lines);
          }
          let cmd = "xdpyinfo 2>/dev/null | grep 'depth of root window' | awk '{ print $5 }'";
          exec(cmd, function (error, stdout) {
            let depth = 0;
            if (!error) {
              let lines = stdout.toString().split('\n');
              depth = parseInt(lines[0]) || 0;
            }
            let cmd = 'xrandr --verbose 2>/dev/null';
            exec(cmd, function (error, stdout) {
              if (!error) {
                let lines = stdout.toString().split('\n');
                result.displays = parseLinesLinuxDisplays(lines, depth);
              }
              if (callback) {
                callback(result)
              }
              resolve(result);
            })
          })
        })
      }
      if (_windows) {
        // https://blogs.technet.microsoft.com/heyscriptingguy/2013/10/03/use-powershell-to-discover-multi-monitor-information/
        exec("wmic path win32_VideoController get AdapterCompatibility, AdapterDACType, name, PNPDeviceID, CurrentVerticalResolution, CurrentHorizontalResolution, CurrentNumberOfColors, AdapterRAM, CurrentBitsPerPixel, CurrentRefreshRate, MinRefreshRate, MaxRefreshRate, VideoMemoryType /value", function (error, stdout) {
          if (!error) {
            let csections = stdout.split(/\n\s*\n/);
            result.controllers = parseLinesWindowsControllers(csections);
            exec("wmic path win32_desktopmonitor get MonitorManufacturer, ScreenWidth, ScreenHeight /value", function (error, stdout) {
              let dsections = stdout.split(/\n\s*\n/);
              if (!error) {
                result.displays = parseLinesWindowsDisplays(dsections);
                if (result.controllers.length === 1 && result.displays.length === 1) {
                  if (_resolutionx && !result.displays[0].resolutionx) {
                    result.displays[0].resolutionx = _resolutionx
                  }
                  if (_resolutiony && !result.displays[0].resolutiony) {
                    result.displays[0].resolutiony = _resolutiony
                  }
                  if (_pixeldepth) {
                    result.displays[0].pixeldepth = _pixeldepth
                  }
                }
              }
              if (callback) {
                callback(result)
              }
              resolve(result);
            })
          }
        })
      }
    });
  });

  function parseLinesWindowsControllers(sections){
    let controllers = [];
    for (let i in sections) {
      if (sections.hasOwnProperty(i)) {
        if (sections[i].trim() !== "") {

          let lines = sections[i].trim().split('\r\n');
          controllers.push({
            model: getValue(lines, 'name', '='),
            vendor: getValue(lines, 'AdapterCompatibility', '='),
            bus: getValue(lines, 'PNPDeviceID', '=').startsWith('PCI') ? 'PCI' : '',
            vram: getValue(lines, 'AdapterRAM', '='),
            vramDynamic: (getValue(lines, 'VideoMemoryType', '=') === '2')
          });
          _resolutionx = toInt(getValue(lines, 'CurrentHorizontalResolution', '='));
          _resolutiony = toInt(getValue(lines, 'CurrentVerticalResolution', '='));
          _pixeldepth = toInt(getValue(lines, 'CurrentBitsPerPixel', '='));
        }
      }
    }
    return controllers;
  }

  function parseLinesWindowsDisplays(sections){
    let displays = [];
    for (let i in sections) {
      if (sections.hasOwnProperty(i)) {
        if (sections[i].trim() !== "") {
          let lines = sections[i].trim().split('\r\n');
          displays.push({
            model: getValue(lines, 'MonitorManufacturer', '='),
            resolutionx: toInt(getValue(lines, 'ScreenWidth', '=')),
            resolutiony: toInt(getValue(lines, 'ScreenHeight', '=')),
          });
        }
      }
    }
    return displays;
  }
}

exports.graphics = graphics;
