#!/usr/bin/python3
# coding=utf-8
#
# See LICENSE.
#
# Copy me if you can.
# by 20h
#

import os
import sys
import re
import getopt
import click
import codecs
from subprocess import Popen, PIPE

DEFAULTBASE = "~/.psw"

class git(object):
	def __init__(self, base=None):
		if base == None:
			base = DEFAULTBASE
		self.base = os.path.expanduser(base)
		if not os.path.exists(self.base):
			os.makedirs(self.base, 0o750)

		if self.base[-1] != os.sep:
			self.pages = "%s%s" % (self.base, os.sep)
		else:
			self.pages = self.base

		self.keycache = None

	""" I/O helper functions. """
	def getfile(self, fi):
		fd = codecs.open(fi, mode="r", encoding="utf-8")
		text = fd.read()
		fd.close()
		return text

	def putfile(self, fi, content):
		os.makedirs(os.path.dirname(fi), exist_ok=True)
		fd = codecs.open(fi, mode="w+", encoding="utf-8")
		fd.write(content)
		fd.close()
		return None

	def mkpath(self, path, fi=None):
		if path[-1] != os.sep:
			path = "%s%s" % (path, os.sep)
		if fi != None:
			path = "%s%s" % (path, fi)

		return path

	def recursedir(self, path):
		subdirs = []
		files = os.listdir(path)
		files.sort()
		for f in files:
			npath = self.mkpath(path, f)
			if os.path.isfile(npath) or \
					os.path.islink(npath):
				yield npath
			else:
				subdirs.append(f)
		
		for s in subdirs:
			npath = self.mkpath(path, s)
			if s == ".git":
				continue
			for j in self.recursedir(npath):
				yield j
	
	def makesearchlist(self, fun, path):
		li = []
		npath = self.mkpath(path)
		for i in fun(path):
			i = i.replace(npath, "")
			i = i.removesuffix(".md")
			li.append(i)
		return li

	""" Git command functions. """
	def git(self, args):
		gitdir = "%s.git" % (self.pages)
		workdir = self.pages
		cmd = ["git", "--git-dir=%s" % gitdir, "--work-tree=%s" % workdir] + args

		p = Popen(cmd, stdout=PIPE, stderr=PIPE)
		result = p.stdout.read().decode("utf-8", errors="replace")

		return [result, p.wait()]

	def gitpath(self, file):
		return "%s%s.md" % (self.pages, file)

	def init(self):
		self.git(["init"])

	def add(self, file):
		self.git(["add", file])

	def rm(self, file):
		self.git(["rm", file])

	def mv(self, old, new):
		self.git(["mv", old, new])

	def commit(self, msg):
		self.git(["commit", "--allow-empty", "--no-verify",
				"--message=%s" % msg, "--author=psw <psw@psw>"])
		self.git(["gc"])
	
	def log(self, page):
		changes = []
		if page == "":
			extra = []
		else:
			extra = ["--", self.gitpath(page)]
		sep = "\x01"
		(result, status) = self.git(
			["log", "--pretty=format:%%H%s%%T%s%%an%s%%ae%s%%aD%s%%s" % ((sep,) * 5)] + extra
		)
		for line in result.splitlines():
			if not line.strip():
				continue
			entries = line.split(sep)
			if len(entries) < 6:
				continue
			change = {}
			change["commit"] = entries[0]
			change["author"] = entries[2]
			change["email"] = entries[3]
			change["date"] = entries[4]
			change["message"] = entries[5]
			try:
				(task, cpage) = entries[5].split(" ", 1)
			except ValueError:
				cpage = page
			change["page"] = cpage
			changes.append(change)

		return changes

	def showcommit(self, page, commit):
		(file, status) = self.git(["cat-file", "-p", "%s:%s.md" % (commit, page)])
		return file

	""" Dictionary abstraction functions. """
	def __setitem__(self, page, value):
		file = self.gitpath(page)
		needadd = False
		if os.path.exists(file) == False:
			needadd = True
		self.putfile(file, value)

		self.add(file)
		if needadd:
			self.commit("Added: %s" % (page))
		else:
			self.commit("Changed: %s" % (page))

	def __delitem__(self, page):
		file = self.gitpath(page)
		os.remove(file)

		self.rm(file)
		self.commit("Deleted: %s" % (page))

	def __getitem__(self, page):
		try:
			return self.getfile(self.gitpath(page))
		except IOError as err:
			return ""

	def getlog(self, page):
		log = self.log(page)
		ret = ""
		for i in log:
			ret += "%s %s %s %s %s %s\n" % (i["commit"],
					i["author"],
					i["email"], i["date"], i["page"],
					i["message"])
		return ret

	def mkkeycache(self):
		if self.keycache == None:
			self.keycache = self.makesearchlist(self.recursedir,
					self.pages)
	
	def keys(self):
		self.mkkeycache()
		return self.keycache

	def __contains__(self, item):
		return item in list(self.keys())

	def search(self, query):
		try:
			pattern = re.compile(query)
		except re.error as e:
			sys.stderr.write("Invalid regex: %s\n" % e)
			return []

		name_hits = []
		content_hits = []
		seen = set()

		for page in list(self.keys()):
			if pattern.search(page):
				name_hits.append(page)
				seen.add(page)

		for page in list(self.keys()):
			if page in seen:
				continue
			try:
				content = self.getfile(self.gitpath(page))
			except OSError:
				continue
			for line in content.splitlines():
				if pattern.search(line):
					content_hits.append("%s: %s" % (page, line.strip()))
					seen.add(page)
					break

		return name_hits + content_hits

	def move(self, old, new):
		opath = self.gitpath(old)
		npath = self.gitpath(new)
		self.mv(opath, npath)
		self.commit("Moved: %s -> %s" % (old, new))

def editor(content):
	try:
		data = click.edit(content, require_save=True, extension='.md')
	except click.UsageError:
		return (1, content)
	if data == None:
		return (1, content)

	return (0, data)

def usage(app):
	app = os.path.basename(app)
	sys.stderr.write("usage: %s [-oh] [-b base] [[-d|-e|-c|-s|-p] item" \
			"|-l|-r old new]\n" % (app))
	sys.exit(1)

def main(args):
	try:
		opts, largs = getopt.getopt(args[1:], "hosplb:sdecr")
	except getopt.GetoptError as err:
		print(str(err))
		usage(args[0])
	
	dorm = False
	doedit = False
	docommit = False
	dosearch = False
	dolist = False
	dorename = False
	tostdout = False
	base = DEFAULTBASE 
	for o, a in opts:
		if o == "-h":
			usage(args[0])
		elif o == "-b":
			base = a
		elif o == "-c":
			docommit = True
		elif o == "-d":
			dorm = True
		elif o == "-e":
			doedit = True
		elif o == "-l":
			dolist = True
		elif o == "-o":
			doedit = True
			tostdout = True
		elif o == "-p":
			doedit = True
			tostdout = True
		elif o == "-r":
			dorename = True
		elif o == "-s":
			dosearch = True
		else:
			assert False, "unhandled option"

	val = ""
	if doedit == True or dosearch == True or dorm == True or docommit == True:
		if len(largs) < 1 and not docommit:
			usage(args[0])
		val = "_".join(largs)
	elif dorename == True:
		if len(largs) < 2:
			usage(args[0])

	lgit = git(base)

	if doedit == True:
		content = lgit[val]
		if tostdout == True:
			sys.stdout.write(content)
		else:
			(sts, data) = editor(content)
			if data == content:
				print("No changes made. Quitting.")
			else:
				print("Some changes made. Committing.")
				lgit[val] = data
	elif dorename == True:
		lgit.move(largs[0], largs[1])
	elif dorm == True:
		del lgit[val]
	elif docommit == True:
		commits = lgit.getlog(val)
		sys.stdout.write(commits)
	elif dosearch == True:
		results = lgit.search(val)
		for r in results:
			print(r)
	elif dolist == True:
		for k in list(lgit.keys()):
			print(k)
	else:
		usage(args[0])
	
	return 0

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

