#!/usr/bin/python
#qtbigtext
#Copyright 2012 Elliot Wolk
# 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
# (at your option) any later version.

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from dbus.mainloop.glib import DBusGMainLoop
import dbus
import dbus.service
import os
import re
import sys
import fcntl
import signal
import threading

signal.signal(signal.SIGINT, signal.SIG_DFL)

CONF = os.getenv("HOME") + '/.config/qtbigtext.conf'
DEFAULT_CONFIG = {
  'lineSeparator': 'false',
  'bgColor': 'black',
  'fgColor': 'white',
  'textFile': os.getenv("HOME") + '/MyDocs/qtbigtext.txt',
  'wordWrap': 'true',
  'typeface': 'Inconsolata',
  'minFontPt': '4',
  'maxFontPt': '600',
  'rotate': 'false',
  'fullScreen': 'true',
  'forceWidth': '',
  'forceHeight': '',
  'align': 'left',
}

class LineType:
  normal = 1
  separator = 2

sampleText = ("The quick brown fox jumped over the lazy dog.")

name = sys.argv[0]
usage = ("Usage:\n"
  + "  [OPTS]" + name + " TEXT [TEXT .. TEXT]  show 'TEXT TEXT ..'\n"
  + "  " + name + " -h  show this message\n"
  + "\n"
  + "  OPTS are --KEY=VAL {VAL can be empty}, and override config file:\n"
  + "    " + CONF + "\n"
  + "  default values are as follows:\n"
  + '\n'.join("    --"+k+"="+v for k, v in sorted(DEFAULT_CONFIG.items()))
)

def printErr(msg):
  sys.stderr.write(msg + "\n")

def readStdin():
  fd = sys.stdin.fileno()
  fl = fcntl.fcntl(fd, fcntl.F_GETFL)
  fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
  sBuf = ''
  while True:
    try:
      if sys.version_info[0] >= 3:
        s = os.read(fd, 512).decode('utf8')
      else:
        s = unicode(os.read(fd, 512), 'utf8')
    except:
      s = ''
    sBuf += s
    if s == "":
      break
  return sBuf

def readFile(path):
  try:
    with open(path) as f:
      return f.read()
  except IOError:
    return ''

def main():
  if len(sys.argv) == 2 and sys.argv[1] == '-h':
    print(usage)
    return 0
  else:
    config = Config()
    config.read()
    args = sys.argv[1:]
    while len(args) > 0 and args[0].startswith('--'):
      arg = args.pop(0)
      arg = re.sub("^--", "", arg)
      config.update(arg)

    conf = config.get()

    if len(args) > 0:
      s = ' '.join(args)
    else:
      s = readStdin()
    s = s.replace("\t", "    ")

    if s == "":
      s = readFile(conf['textFile'])
    if s == "":
      s = sampleText

    app = QApplication([])
    app.setStyleSheet("QWidget { background-color : %s; color : %s;}" % (
      conf['bgColor'], conf['fgColor']))

    if conf['rotate'].lower() == 'true':
      (w, h) = (conf['forceWidth'], conf['forceHeight'])
      conf['forceWidth'] = h
      conf['forceHeight'] = w

    qtBigText = QtBigText(conf)
    qtBigText.setText(s)

    if conf['rotate'].lower() == 'true':
      graphicsView = QGraphicsView()
      scene = QGraphicsScene(graphicsView)
      graphicsView.setScene(scene)

      proxy = QGraphicsProxyWidget()
      proxy.setWidget(qtBigText)
      proxy.setTransformOriginPoint(proxy.boundingRect().center())
      proxy.setRotation(90)
      scene.addItem(proxy)

      widget = graphicsView
    else:
      widget = qtBigText

    if conf['fullScreen'].lower() == "false":
      widget.show()
    else:
      widget.showFullScreen()

    DBusGMainLoop(set_as_default=True)
    QtBigTextDbusService(qtBigText)
    app.exec_()

class Config():
  def __init__(self):
    self.default()
  def default(self):
    self.conf = DEFAULT_CONFIG.copy()
  def get(self):
    return self.conf
  def read(self):
    try:
      with open(CONF, 'r') as f:
        for line in f:
          self.update(line)
    except IOError:
      self.default()
      self.write()
  def update(self, entry):
    m = re.search('\\s*([a-zA-Z]+)\\s*=\\s*(.*)', entry)
    if m != None:
      k = m.group(1)
      v = m.group(2)
      if k in self.conf:
        self.conf[k] = v
      else:
        printErr("Malformed or unknown option: " + k + "=" + v)
  def write(self):
    msg = ''
    for k,v in sorted(self.conf.items()):
      msg += k + '=' + v + "\n"
    try:
      with open(CONF, 'w') as f:
        f.write(msg)
    except IOError as e:
      printErr(e)

class QtBigTextDbusService(dbus.service.Object):
  def __init__(self, qtbigtext):
    dbus.service.Object.__init__(self, self.getBusName(), '/')
    self.qtbigtext = qtbigtext
    self.lock = threading.Lock()
  def getBusName(self):
    return dbus.service.BusName(
      'org.teleshoes.qtbigtext', bus=dbus.SessionBus())
  @dbus.service.method('org.teleshoes.qtbigtext')
  def test(self):
    pass
  @dbus.service.method('org.teleshoes.qtbigtext')
  def setText(self, text):
    self.lock.acquire()
    try:
      self.qtbigtext.setText(text)
    finally:
      self.lock.release()

class QtBigText(QWidget):
  def __init__(self, conf):
    QWidget.__init__(self)
    self.conf = conf
    self.layout = QVBoxLayout(self)
    self.labelCache = []
    self.frameCache = []
    self.fontCache = {}
    self.fontMetricCache = {}
    self.geometry = QDesktopWidget().availableGeometry()
    if len(self.conf['forceWidth']) > 0:
      w = int(self.conf['forceWidth'])
      self.setFixedWidth(w)
      self.geometry.setWidth(w)
    if len(self.conf['forceHeight']) > 0:
      h = int(self.conf['forceHeight'])
      self.setFixedHeight(h)
      self.geometry.setHeight(h)
    self.setContentsMargins(0,0,0,0)

    minPt = int(self.conf['minFontPt'])
    maxPt = int(self.conf['maxFontPt'])

    self.fontDecaPts = []
    decaPt = minPt*10
    while decaPt < maxPt*10:
      self.fontDecaPts.append(decaPt)
      decaPt += 1
    self.guessFontPtIndex = None
  def setText(self, text):
    self.clear()
    font = self.constructFont(self.selectPointSize(text))
    grid = self.parseGrid(text, font)

    if grid == None:
      printErr("text too big\n")
      text = "!"
      font = self.constructFont(self.selectPointSize(text))
      grid = self.parseGrid(text, font)
      if grid == None:
        printErr("failure: could not fit one character on screen\n")
        sys.exit(1)

    for row in grid:
      [line, lineType] = row
      self.layout.addWidget(self.createLabel(line, font))
      if lineType == LineType.separator:
        self.layout.addWidget(self.createSeparator())
  def createLabel(self, text, font):
    if len(self.labelCache) == 0:
      label = QLabel()
      label.setWordWrap(False)
      style = self.getAlignStyle()
      if style != None:
        label.setStyleSheet(style)
      self.labelCache.append(label)
    label = self.labelCache.pop()
    label.setText(text)
    label.setFont(font)
    return label
  def getAlignStyle(self):
    align = self.conf['align'].lower()
    if align == 'left':
      return 'qproperty-alignment: AlignLeft;'
    elif align == 'center':
      return 'qproperty-alignment: AlignCenter;'
    elif align == 'right':
      return 'qproperty-alignment: AlignRight;'
    else:
      return None
  def createSeparator(self):
    if len(self.frameCache) == 0:
      sep = QFrame()
      sep.setFrameShape(QFrame.HLine)
      self.frameCache.append(sep)
    sep = self.frameCache.pop()
    return sep
  def clear(self):
    while self.layout.count() > 0:
      w = self.layout.takeAt(0).widget()
      w.setParent(None)
      if isinstance(w, QLabel):
        self.labelCache.append(w)
      elif isinstance(w, QFrame):
        self.frameCache.append(w)
  def screenWidth(self):
    return self.geometry.width()
  def screenHeight(self):
    return self.geometry.height()
  def calculateGrid(self, font):
    fm = self.constructFontMetrics(font)
    w = fm.width('W')
    h = fm.height()
    rows = int(self.screenHeight() / h)
    cols = int(self.screenWidth() / w)
    return (rows, cols)
  def parseGrid(self, text, font):
    rows, cols = self.calculateGrid(font)
    if len(text) > (rows * cols):
      return None
    else:
      grid = self.wordWrap(text, cols)
      if len(grid) > rows:
        return None
      else:
        return grid
  def wordWrap(self, text, cols):
    rows = []
    start = 0
    end = start + cols
    length = len(text)
    isWordWrap = self.conf['wordWrap'].lower() == "true"
    isLineSeparator = self.conf['lineSeparator'].lower() == "true"
    for i in range(length):
      c = text[i]
      lineBreak = False
      if c == "\n":
        end = i+1
        lineBreak = True
      elif isWordWrap and c == " ":
        end = i+1

      if i - start >= cols or lineBreak:
        line = text[start:end]
        rows.append(self.getRow(line, lineBreak, isLineSeparator))
        start = end
        end = start + cols

      if start+cols >= length:
        for line in text[start:].split("\n"):
          rows.append(self.getRow(line, True, isLineSeparator))
        break

    #remove empty trailing lines
    while len(rows) > 0 and rows[-1][0] == "":
      del rows[-1]

    #remove separator from last line
    if len(rows) > 0:
      rows[-1][1] = LineType.normal

    return rows
  def getRow(self, text, isLineBreak, isLineSeparator):
    if isLineBreak and isLineSeparator:
      lineType = LineType.separator
    else:
      lineType = LineType.normal
    text = text.replace('\n', '')
    return [text, lineType]
  def splitAt(self, s, n):
    for i in range(0, len(s), n):
      yield s[i:i+n]
  def textFits(self, text, font):
    return self.parseGrid(text, font) != None
  def constructFont(self, pointSize):
    if not pointSize in self.fontCache:
      font = QFont()
      font.setFamily(self.conf['typeface'])
      font.setPointSizeF(pointSize)
      font.setStyleStrategy(QFont.PreferAntialias)
      self.fontCache[pointSize] = font
    return self.fontCache[pointSize]
  def constructFontMetrics(self, font):
    p = font.pointSizeF()
    if not p in self.fontMetricCache:
      self.fontMetricCache[p] = QFontMetrics(font)
    return self.fontMetricCache[p]
  def testIndex(self, text, decaFontPtIndex):
    if decaFontPtIndex < 0:
      return True
    if decaFontPtIndex >= len(self.fontDecaPts):
      return False
    font = self.constructFont(self.fontDecaPts[decaFontPtIndex]/10)
    return self.textFits(text, font)
  def selectPointSize(self, text):
    minIndex = 0
    maxIndex = len(self.fontDecaPts)

    if self.guessFontPtIndex != None:
      midIndex = self.guessFontPtIndex

      #check guess index to see if its exactly correct
      if self.testIndex(text, midIndex):
        minIndex = midIndex
        if not self.testIndex(text, midIndex+1):
          maxIndex = midIndex
        midIndex = int((minIndex+maxIndex)/2)
    else:
      midIndex = int((minIndex+maxIndex)/2)

    while minIndex < midIndex:
      if self.testIndex(text, midIndex):
        minIndex = midIndex
      else:
        maxIndex = midIndex-1
      midIndex = int((minIndex + maxIndex) / 2)

    self.guessFontPtIndex = midIndex
    fontPt = self.fontDecaPts[midIndex]/10
    return fontPt

if __name__ == "__main__":
  sys.exit(main())