# -*- coding: utf-8 -*-
#
# This file is part of EventGhost.
# Copyright © 2005-2016 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost 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 2 of the License, or (at your option)
# any later version.
#
# EventGhost 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 EventGhost. If not, see <http://www.gnu.org/licenses/>.


import win32api
import wx
import _winreg

from win32con import (
    DISPLAY_DEVICE_MIRRORING_DRIVER,
    DISPLAY_DEVICE_PRIMARY_DEVICE,
    DISPLAY_DEVICE_REMOVABLE,
    DISPLAY_DEVICE_VGA_COMPATIBLE,

)

try:
    from WindowsVersion import WindowsVersion
except (ValueError, ImportError):
    WindowsVersion = None

DISPLAY_DEVICE_ACTIVE = 0x1
NID_INTEGRATED_TOUCH = 0x01
NID_EXTERNAL_TOUCH = 0x02

DTD = 54
MANUFACTURER_ID_OFFSET = 8
PRODUCT_ID_OFFSET = 10
MANUFACTURER_ID_BITS = 5

WMI_QUERY = (
    "SELECT * FROM Win32_PnPEntity WHERE PNPDeviceID = '%s'"
)
WMI_ATTRIBUTES = (
    'Caption',
    'Description',
    'DeviceID',
    'HardwareID',
    'Manufacturer',
    'Name'
)

WINREG_KEY = 'SYSTEM\\CurrentControlSet\\Enum\\%s\\Device Parameters'

DISPLAY_TEMPLATE = (
    'Vendor Name: {vendor_name}\n'
    'Model: {model}\n'
    'Description: {description}\n'
    'Display Number: {number}\n'
    'Serial Number: {serial_number}\n'
    'MFG Month: {mfg_month}\n'
    'MFG Year: {mfg_year}\n'
    'Width: {width}\n'
    'Height: {height}\n'
    'Native Width: {native_width}\n'
    'Native Height: {native_height}\n'
    'X: {x}\n'
    'Y: {y}\n'
    'Primary: {is_primary}\n'
    'Active: {is_active}\n'
    'Blacked Out: {blackout}\n'
    'Mirroring: {is_mirroring}\n'
    'Removable: {is_removable}\n'
    'VGA Compatible: {is_vga_compatible}\n'
    'Vendor Id: {vendor_id}\n'
    'Vendor Code: {vendor_code}\n'
    'Product Id: {product_id}\n'
    'EDID version: {edid_version}'
)


VENDORS = {
    'AAA': 'Avolites Ltd ',
    'ACI': 'Ancor Communications Inc ',
    'ACR': 'Acer Technologies ',
    'ADA': 'Addi-Data GmbH ',
    'APP': 'Apple Computer Inc ',
    'BNO': 'Bang & Olufsen ',
    'CMN': 'Chimei Innolux Corporation ',
    'CMO': 'Chi Mei Optoelectronics corp. ',
    'CRO': 'Extraordinary Technologies PTY Limited ',
    'DEL': 'Dell Inc. ',
    'DON': 'DENON, Ltd. ',
    'ENC': 'Eizo Nanao Corporation ',
    'EPH': 'Epiphan Systems Inc.  ',
    'FUS': 'Fujitsu Siemens Computers GmbH ',
    'GSM': 'Goldstar Company Ltd ',
    'HIQ': 'Kaohsiung Opto Electronics Americas, Inc. ',
    'HSD': 'HannStar Display Corp ',
    'HWP': 'Hewlett Packard ',
    'INT': 'Interphase Corporation ',
    'IVM': 'Iiyama North America ',
    'LEN': 'Lenovo Group Limited ',
    'MAX': 'Rogen Tech Distribution Inc ',
    'MEG': 'Abeam Tech Ltd ',
    'MEI': 'Panasonic Industry Company ',
    'MTC': 'Mars-Tech Corporation ',
    'MTX': 'Matrox ',
    'NEC': 'NEC Corporation ',
    'ONK': 'ONKYO Corporation ',
    'ORN': 'ORION ELECTRIC CO., LTD. ',
    'OTM': 'Optoma Corporation           ',
    'OVR': 'Oculus VR, Inc. ',
    'PHL': 'Philips Consumer Electronics Company ',
    'PIO': 'Pioneer Electronic Corporation ',
    'PNR': 'Planar Systems, Inc. ',
    'QDS': 'Quanta Display Inc. ',
    'SAM': 'Samsung Electric Company ',
    'SEC': 'Seiko Epson Corporation ',
    'SHP': 'Sharp Corporation ',
    'SII': 'Silicon Image, Inc. ',
    'SNY': 'Sony ',
    'TOP': 'Orion Communications Co., Ltd. ',
    'TSB': 'Toshiba America Info Systems Inc ',
    'TST': 'Transtream Inc ',
    'UNK': 'Unknown ',
    'VIZ': 'VIZIO, Inc ',
    'VSC': 'ViewSonic Corporation ',
    'YMH': 'Yamaha Corporation ',
}


def _decode_edid(device_id):
    key = _winreg.OpenKey(
        _winreg.HKEY_LOCAL_MACHINE,
        WINREG_KEY % device_id
    )

    edid = _winreg.QueryValueEx(key, 'EDID')[0]
    from datetime import datetime
    year = ord(edid[17]) - 10
    if year < 0:
        year += 10
        year = '19' + str(year).zfill(2)
    else:
        year = '20' + str(year).zfill(2)
    week = str(ord(edid[16]))

    mfg_date = datetime.strptime('%s %s 0' % (year, week), "%Y %W %w")
    mfg_year = mfg_date.strftime('%Y')
    mfg_month = mfg_date.strftime('%B')

    edid_version = '{0}.{1}'.format(ord(edid[18]), ord(edid[19]))

    def read_short_be(offset, blob):
        return (ord(blob[offset]) << 8) | ord(blob[offset + 1])

    def read_short_le(offset, blob):
        return (ord(blob[offset + 1]) << 8) | ord(blob[offset])

    vendor_id = ''
    vendor_code = read_short_be(MANUFACTURER_ID_OFFSET, edid)
    for i in range(2, -1, -1):
        vendor_char = (vendor_code >> (i * MANUFACTURER_ID_BITS)) & 0x1F
        vendor_char = chr(vendor_char + ord('@'))
        vendor_id += vendor_char
    vendor_code = hex(vendor_code)

    if vendor_id in VENDORS:
        vendor_name = VENDORS[vendor_id]
    else:
        vendor_name = ''

    product_id = hex(read_short_le(PRODUCT_ID_OFFSET, edid))

    native_width = ((ord(edid[DTD + 4]) >> 4) << 8) | ord(edid[DTD + 2])
    native_height = ((ord(edid[DTD + 7]) >> 4) << 8) | ord(edid[DTD + 5])

    desc = edid[54:126]
    model = ''
    serial_number = ''

    for i in range(0, 72, 18):
        if desc[i:5 + i] == b'\x00\x00\x00\xFC\x00':
            model = desc[5 + i:13 + 5 + i].strip()
        elif desc[i:5 + i] == b'\x00\x00\x00\xFF\x00':
            serial_number = desc[5 + i:13 + 5 + i]

    return {
        'edid_version': edid_version,
        'vendor_name': vendor_name,
        'vendor_id': vendor_id,
        'vendor_code': vendor_code,
        'product_id': product_id,
        'native_width': native_width,
        'native_height': native_height,
        'model': model,
        'serial_number': serial_number.strip(),
        'mfg_year': mfg_year,
        'mfg_month': mfg_month
    }


class BlackoutFrame(wx.Frame):

    def __init__(self, handler):
        self.handler = handler
        wx.Frame.__init__(
            self,
            None,
            -1,
            style=wx.BORDER_NONE | wx.STAY_ON_TOP,
            pos=(handler.x, handler.y),
            size=(handler.w, handler.h)
        )

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Show()

    def OnPaint(self, evt=None):
        try:
            size = (self.handler.w, self.handler.h)
            pos = (self.handler.x, self.handler.y)
        except DisplayException:
            self.handler.blackout = False
            return

        if self.GetSizeTuple() != size:
            self.SetSize(size)

        if self.GetPositionTuple() != pos:
            self.SetPosition(pos)

        bmp = wx.EmptyBitmap(*size)
        dc = wx.MemoryDC()
        dc.SelectObject(bmp)
        dc.SetBackground(wx.Brush(wx.Colour(0, 0, 0)))
        dc.Destroy()
        del dc

        if evt is not None:
            dc = wx.PaintDC(self)
            dc.DrawBitmap(bmp, 0, 0)


class Display(object):

    def __init__(self, idx):
        self._idx = idx
        self._frame = None

    @property
    def _handle(self):
        displays = win32api.EnumDisplayMonitors()

        if len(displays) - 1 >= self._idx:
            return displays[self._idx][0]

        if self._frame is not None:
            self._frame.Destroy()
            self._frame = None

        raise DesktopDisplay.DisplayNumberError(self._idx)

    @property
    def blackout(self):
        return self._frame is not None

    @blackout.setter
    def blackout(self, flag):
        if flag and self._frame is None:
            self._frame = BlackoutFrame(self)
        elif not flag and self._frame:
            self._frame.Destroy()
            self._frame = None

    def _metrics(self, idx1, idx2=None):
        metrics = win32api.GetMonitorInfo(self._handle)
        if idx2 is None:
            return metrics['Monitor'][idx1]
        else:
            return metrics['Monitor'][idx1] + -metrics['Monitor'][idx2]

    @property
    def x(self):
        return self._metrics(0)

    @property
    def y(self):
        return self._metrics(1)

    @property
    def w(self):
        return self._metrics(2, 0)

    @property
    def h(self):
        return self._metrics(3, 1)

    @property
    def _state_flags(self):
        metrics = win32api.GetMonitorInfo(self._handle)
        return win32api.EnumDisplayDevices(metrics['Device']).StateFlags

    @property
    def is_mirroring(self):
        state_flags = self._state_flags
        return state_flags | DISPLAY_DEVICE_MIRRORING_DRIVER == state_flags


    @property
    def is_primary(self):
        state_flags = self._state_flags
        return state_flags | DISPLAY_DEVICE_PRIMARY_DEVICE == state_flags

    @property
    def is_vga_compatible(self):
        state_flags = self._state_flags
        return state_flags | DISPLAY_DEVICE_VGA_COMPATIBLE == state_flags

    @property
    def is_active(self):
        state_flags = self._state_flags
        return state_flags | DISPLAY_DEVICE_ACTIVE == state_flags

    @property
    def is_removable(self):
        state_flags = self._state_flags
        return state_flags | DISPLAY_DEVICE_REMOVABLE == state_flags

    @property
    def number(self):
        metrics = win32api.GetMonitorInfo(self._handle)
        display = win32api.EnumDisplayDevices(metrics['Device'])
        return int(display.DeviceName[4:].split('\\')[0][7:])

    @property
    def description(self):
        metrics = win32api.GetMonitorInfo(self._handle)
        display = win32api.EnumDisplayDevices(metrics['Device'])
        return display.DeviceString

    @property
    def edid(self):
        metrics = win32api.GetMonitorInfo(self._handle)
        display = win32api.EnumDisplayDevices(metrics['Device'], 0, 1)
        device_id = display.DeviceID.split('#')
        device_id[0] = device_id[0].lstrip(r'\?')
        pnp_device_id = "{0}\\{1}\\{2}".format(*device_id[:3])
        return _decode_edid(pnp_device_id)

    @property
    def size(self):
        return self.w, self.h

    @property
    def pos(self):
        return self.x, self.y

    @property
    def native_width(self):
        return self.edid['native_width']

    @property
    def native_height(self):
        return self.edid['native_height']

    @property
    def mfg_year(self):
        return self.edid['mfg_year']

    @property
    def mfg_month(self):
        return self.edid['mfg_month']

    @property
    def serial_number(self):
        return self.edid['serial_number']

    @property
    def product_id(self):
        return self.edid['product_id']

    @property
    def edid_version(self):
        return self.edid['edid_version']

    @property
    def vendor_name(self):
        return self.edid['vendor_name']

    @property
    def vendor_id(self):
        return self.edid['vendor_id']

    @property
    def vendor_code(self):
        return self.edid['vendor_code']

    @property
    def model(self):
        return self.edid['model']

    def __str__(self):
        data = {
            'description': self.description,
            'number': self.number,
            'width': self.w,
            'height': self.h,
            'x': self.x,
            'y': self.y,
            'is_primary': self.is_primary,
            'is_removable': self.is_removable,
            'is_active': self.is_active,
            'is_vga_compatible': self.is_vga_compatible,
            'is_mirroring': self.is_mirroring,
            'blackout': self.blackout,
        }
        data.update(self.edid)

        return DISPLAY_TEMPLATE.format(**data)


class DisplayException(Exception):
    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return str(self.msg)


class DesktopDisplay(object):

    DisplayException = DisplayException
    Display = Display

    class DisplayNameError(DisplayException):
        pass


    class DisplayNumberError(DisplayException):
        pass

    class FindDisplayError(DisplayException):
        pass


    def __init__(self):
        self._displays = []
        self.Refresh()

    def Refresh(self):
        displays = []

        for i, display_handles in enumerate(win32api.EnumDisplayMonitors()):
            metrics = win32api.GetMonitorInfo(display_handles[0])
            display = win32api.EnumDisplayDevices(metrics['Device'])
            try:
                if self._displays[i]['key'] != display.DeviceKey:
                    if self._displays[i]['display'].blackout:
                        self._displays[i]['display'].blackout = False

                self._displays[i]['key'] = display.DeviceKey
                displays += [self._displays[i]]

            except IndexError:
                displays += [dict(key=display.DeviceKey, display=Display(i))]
        for display_data in self._displays[:]:
            if display_data not in displays:
                display = display_data['display']
                if display.blackout:
                    display.blackout = False

                self._displays.remove(display_data)
        self._displays = displays[:]

    def EnumDisplays(self):
        self.Refresh()
        return list(display['display'] for display in self._displays)

    def GetDisplay(
        self,
        number=None,
        vendor=None,
        model=None,
        description=None,
        serial=None
    ):
        displays = self.EnumDisplays()
        high_match_count = 0
        matched_display = None

        if number is not None:
            number -= 1

        for i, display in enumerate(displays):
            match_count = 0

            if display.description == description:
                match_count += 1
            if display.vendor_name == vendor:
                match_count += 1
            if display.model == model:
                match_count += 1
            if display.serial_number == serial:
                match_count += 1
            if match_count > high_match_count:
                high_match_count = match_count
                matched_display = display
            if number == i and matched_display is None:
                matched_display = display

        if matched_display is None:
            if number is None:
                raise DesktopDisplay.FindDisplayError('')
            else:
                raise DesktopDisplay.DisplayNumberError(number + 1)
        return matched_display

    @property
    def size(self):
        return win32api.GetSystemMetrics(78), win32api.GetSystemMetrics(79)

    @property
    def displayCount(self):
        return win32api.GetSystemMetrics(80)

    @property
    def isTouchScreen(self):
        if WindowsVersion is None:
            return None
        try:
            if win32api.GetSystemMetrics(86):
                if WindowsVersion >= '7':
                    touch = win32api.GetSystemMetrics(94)
                    if touch in (NID_INTEGRATED_TOUCH, NID_EXTERNAL_TOUCH):
                        return True
                    return False
                return True
        except:
            pass
        return False

    @property
    def isMultiTouch(self):
        return self.maxTouchCount > 1

    @property
    def maxTouchCount(self):
        if self.isTouchScreen:
            return win32api.GetSystemMetrics(95)
        else:
            return 0

    @property
    def displays(self):
        return self.EnumDisplays()

    def __len__(self):
        return self.displayCount

DesktopDisplay = DesktopDisplay()


if __name__ == "__main__":
    print 'Number of displays:', len(DesktopDisplay)
    print
    for d in DesktopDisplay.displays:
        print d
        print'\n\n-------------------------------\n\n'
