from __future__ import print_function
import pygit2
#   brew install libgit2
#   pip install pygit2
import tempfile # import mkstemp
import os # import remove, close, path, stat, chmod, urandom, mkdir, devnull
import shutil # import move, rmtree
import hashlib # import sha1
import time # import time
import subprocess # import call, STDOUT
import sys # import stderr 
from config import * # global variables

def init_repo(repo_dir, remote_uri):
  """initialize (destructive) a repo into repo_dir from remote_uri"""
  shutil.rmtree(repo_dir, ignore_errors=True)
  os.mkdir(repo_dir)
  # pygit2 doesn't like the way stripe's git repo is setup
  # I don't care enough to actually figure out why...
  # any feedback on this would be appreciated :)
  subprocess.call(["git", "clone", remote_uri, repo_dir], 
                  stdout=open(os.devnull,'w'), 
                  stderr=subprocess.STDOUT)
  print("gitchain init complete", file=sys.stderr)
  return pygit2.init_repository(repo_dir)

def update_ledger(ledger_path, username):
  """add one gitcoin to the username in the ledger file at ledger_path"""
  coin_added = False
  ledger = open(ledger_path, "r")
  fh, abs_path = tempfile.mkstemp()
  new_ledger = open(abs_path, "w")
  for line in ledger:
    if username in line:
      new_ledger.write(username+": "+str(int(line.split(": ")[1])+1)+"\n")
      coin_added = True
    else:
      new_ledger.write(line)
  if not coin_added:
    new_ledger.write(username+": 1\n")
  new_ledger.close()
  os.close(fh)
  ledger.close()
  os.remove(ledger_path)
  shutil.move(abs_path, ledger_path)
  os.chmod(ledger_path, 0644)

def fastforward_repo(repo, repo_dir, remote_branch):
  """reset repository at repo_dir and return the resulting pygit2 repo object"""
  # see previous about fetching issues with pygit2
  subprocess.call(["git", "fetch", "--all"], 
                  cwd=repo_dir, 
                  stdout=open(os.devnull,'w'), 
                  stderr=subprocess.STDOUT)
  fastforward = False
  # get the oid of the remote branch, if != current head fastforward
  branch = repo.lookup_branch(remote_branch, pygit2.GIT_BRANCH_REMOTE)
  if branch.target.hex != repo.head.target.hex:
    print("fast forwarding gitchain", file=sys.stderr)
    # merge to remote, if conflicts occur it is because a genesis event 
    try: merge_result = repo.merge(repo.get(branch.target.hex).oid)
    except pygit2.GitError:
      print("gitchain had a genesis event: fetching!", file=sys.stderr)
      return init_repo(repo_dir, repo.remotes[0].url), True
    if merge_result.is_fastforward:
      repo.reset(merge_result.fastforward_oid, pygit2.GIT_RESET_HARD)
      if (branch.target.hex 
          == merge_result.fastforward_oid.hex 
          == repo.head.target.hex):
        fastforward = True
      else:
        raise Exception("git reset failed!")
    else:
      raise Exception("git merge failed!")
  else:
    print("no gitchian updates...", file=sys.stderr)
  return repo, fastforward

def tree_add_to_repo(repo, repo_dir, filename):
  """
  add filename to repository and return the repo and the tree oid
  unfortunately this broke on commit, so instead see index_add_to_repo
  """
  oid = repo.create_blob_fromworkdir(filename)
  treebuilder = repo.TreeBuilder()
  treebuilder.insert(filename, 
                     oid, 
                     os.stat(os.path.join(repo_dir,filename)).st_mode)
  tree_oid = treebuilder.write()
  return repo, tree_oid

def index_add_to_repo(repo, repo_dir, filename):
  """add filename to repository and return the repo and the tree oid"""
  repo.index.read()
  repo.index.add(filename)
  return repo, repo.index.write_tree() 

def gen_commit_context(repo, tree, username, email, time, msg_len):
  """return the sha1 context lacking only the message portion of the commit"""
  head = repo.head.target.hex
  body = ("tree "+tree+"\n"
          "parent "+head+"\n"
          "author "+username+" <"+email+"> "+str(int(time))+" +0000\n"  
          "committer "+username+" <"+email+"> "+str(int(time))+" +0000\n\n")
  s = hashlib.sha1()
  s.update("commit %u\0" % (len(body)+msg_len))
  s.update(body)
  return s

def gen_commit(repo, tree, username, email, time, message):
  """ useless function that creates the signature and commit """
  sig = pygit2.Signature(name=username, email=email, time=time)
  head = repo.head.target.hex
  return repo.create_commit(repo.head.name, sig, sig, message, tree, [head]) 

def proof_of_work(s, msg_len, difficulty, timeout):
  # TODO: generalize and zmq this work out
  """return message, digest, success""" 
  freq = 168384*2
  start = time.time()
  worker_time = time.time()
  count = 2
  while True:
    count += 1
    message = os.urandom(msg_len/2).encode('hex') 
    tmp_s = s.copy()
    tmp_s.update(message)
    if tmp_s.hexdigest() < difficulty: 
      return message, tmp_s.hexdigest(), True
    if (count % freq) == 1: 
      hashrate = (((1/(time.time()-start))*freq)/1024)
      print("~ %0.3f Kh/s" % hashrate, end='\n', file=sys.stderr)
      start = time.time()
    if (time.time()-worker_time) > timeout:
      return "", "", False

if __name__ == "__main__":
  repo = init_repo(repo_dir=REPO_DIR, remote_uri=REMOTE_URI)
  commit_time = time.time()

  # add gitcoin to the repository and start commit sha1
  difficulty = open(DIFFICULTY_PATH,"r").read().split()[0]
  update_ledger(ledger_path=LEDGER_PATH, username=USERNAME)
  repo, tree = index_add_to_repo(repo=repo, 
                                 repo_dir=REPO_DIR, 
                                 filename=LEDGER_FILENAME)
  s = gen_commit_context(repo=repo, 
                         tree=tree.hex,
                         username=USERNAME, 
                         email=EMAIL, 
                         time=commit_time, 
                         msg_len=MESSAGE_LENGTH)
  
  while True:
  # mining loop
    message, digest, success = proof_of_work(s=s,
                                             msg_len=MESSAGE_LENGTH, 
                                             difficulty=difficulty,
                                             timeout=GIT_POLL_INTERVAL)
    repo, fastforward = fastforward_repo(repo=repo, 
                                         repo_dir=REPO_DIR, 
                                         remote_branch=REMOTE_BRANCH)
    if success and not fastforward:
      print("mined gitcoin! commit-hash:[%s]" % (message, digest),
            file=sys.stderr)
      break
    elif fastforward:
        # re-add our gitcoin to the reset --hard'd repository 
        difficulty = open(DIFFICULTY_PATH,"r").read().split()[0]
        update_ledger(ledger_path=LEDGER_PATH, username=USERNAME)
        repo, tree = index_add_to_repo(repo=repo, 
                                       repo_dir=REPO_DIR, 
                                       filename=LEDGER_FILENAME)
        s = gen_commit_context(repo=repo, 
                               tree=tree.hex,
                               username=USERNAME, 
                               email=EMAIL, 
                               time=commit_time, 
                               msg_len=MESSAGE_LENGTH)
  # end mining loop

  c = gen_commit(repo=repo,
                 tree=tree, 
                 username=USERNAME, 
                 email=EMAIL, 
                 time=commit_time, 
                 message=message)

  # make sure we really have a winner 
  s.update(message)
  if c.hex != s.hexdigest():
    raise Exception("commit hash mismatch!")

  # again, making up for pygit2's shortcomings...
  subprocess.call(["git", "push", "origin", "master"], 
                  cwd=REPO_DIR, 
                  stdout=open(os.devnull,'w'), 
                  stderr=subprocess.STDOUT)
  # maybe dont need separate dirs?
  shutil.rmtree(REPO_DIR, ignore_errors=True)
