# -*-python-*-
#
# Copyright (C) 1999-2023 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------

import os
import re
import cStringIO
import tempfile

import vclib
import rcsparse
import blame

### The functionality shared with bincvs should probably be moved to a
### separate module
from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp, _path_join


class CCVSRepository(BaseCVSRepository):
  def dirlogs(self, path_parts, rev, entries, options):
    """see vclib.Repository.dirlogs docstring

    rev can be a tag name or None. if set only information from revisions
    matching the tag will be retrieved

    Option values recognized by this implementation:

      cvs_subdirs
        boolean. true to fetch logs of the most recently modified file in each
        subdirectory

    Option values returned by this implementation:

      cvs_tags, cvs_branches
        lists of tag and branch names encountered in the directory
    """
    if self.itemtype(path_parts, rev) != vclib.DIR:  # does auth-check
      raise vclib.Error("Path '%s' is not a directory."
                        % (part2path(path_parts)))
    entries_to_fetch = []
    for entry in entries:
      if vclib.check_path_access(self, path_parts + [entry.name], None, rev):
        entries_to_fetch.append(entry)

    subdirs = options.get('cvs_subdirs', 0)

    dirpath = self._getpath(path_parts)
    alltags = {           # all the tags seen in the files of this dir
      'MAIN' : '',
      'HEAD' : '1.1'
    }

    for entry in entries_to_fetch:
      entry.rev = entry.date = entry.author = None
      entry.dead = entry.absent = entry.log = entry.lockinfo = None
      path = _log_path(entry, dirpath, subdirs)
      if path:
        entry.path = path
        try:
          rcsparse.parse(open(path, 'rb'), InfoSink(entry, rev, alltags))
        except IOError, e:
          entry.errors.append("rcsparse error: %s" % e)
        except RuntimeError, e:
          entry.errors.append("rcsparse error: %s" % e)
        except rcsparse.RCSStopParser:
          pass

    branches = options['cvs_branches'] = []
    tags = options['cvs_tags'] = []
    for name, rev in alltags.items():
      if Tag(None, rev).is_branch:
        branches.append(name)
      else:
        tags.append(name)

  def itemlog(self, path_parts, rev, sortby, first, limit, options):
    """see vclib.Repository.itemlog docstring

    rev parameter can be a revision number, a branch number, a tag name,
    or None. If None, will return information about all revisions, otherwise,
    will only return information about the specified revision or branch.

    Option values returned by this implementation:

      cvs_tags
        dictionary of Tag objects for all tags encountered
    """
    if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
      raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))

    path = self.rcsfile(path_parts, 1)
    sink = TreeSink()
    rcsparse.parse(open(path, 'rb'), sink)
    filtered_revs = _file_log(sink.revs.values(), sink.tags, sink.lockinfo,
                              sink.default_branch, rev)
    for rev in filtered_revs:
      if rev.prev and len(rev.number) == 2:
        rev.changed = rev.prev.next_changed
    options['cvs_tags'] = sink.tags

    if sortby == vclib.SORTBY_DATE:
      filtered_revs.sort(_logsort_date_cmp)
    elif sortby == vclib.SORTBY_REV:
      filtered_revs.sort(_logsort_rev_cmp)

    if len(filtered_revs) < first:
      return []
    if limit:
      return filtered_revs[first:first+limit]
    return filtered_revs

  def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
    if self.itemtype(path_parts1, rev1) != vclib.FILE:  # does auth-check
      raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1)))
    if self.itemtype(path_parts2, rev2) != vclib.FILE:  # does auth-check
      raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2)))

    fd1, temp1 = tempfile.mkstemp()
    os.fdopen(fd1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue())
    fd2, temp2 = tempfile.mkstemp()
    os.fdopen(fd2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue())

    r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]
    r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]

    info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
    info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)

    diff_args = vclib._diff_args(type, options)

    return vclib._diff_fp(temp1, temp2, info1, info2,
                          self.utilities.diff or 'diff', diff_args)

  def annotate(self, path_parts, rev=None, include_text=False):
    if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
      raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
    source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, include_text)
    return source, source.revision

  def revinfo(self, rev):
    raise vclib.UnsupportedFeature

  def openfile(self, path_parts, rev, options):
    if self.itemtype(path_parts, rev) != vclib.FILE:  # does auth-check
      raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
    path = self.rcsfile(path_parts, 1)
    sink = COSink(rev)
    rcsparse.parse(open(path, 'rb'), sink)
    revision = sink.last and sink.last.string
    return cStringIO.StringIO(''.join(sink.sstext.text)), revision

class MatchingSink(rcsparse.Sink):
  """Superclass for sinks that search for revisions based on tag or number"""

  def __init__(self, find):
    """Initialize with tag name or revision number string to match against"""
    if not find or find == 'MAIN' or find == 'HEAD':
      self.find = None
    else:
      self.find = find

    self.find_tag = None

  def set_principal_branch(self, branch_number):
    if self.find is None:
      self.find_tag = Tag(None, branch_number)

  def define_tag(self, name, revision):
    if name == self.find:
      self.find_tag = Tag(None, revision)

  def admin_completed(self):
    if self.find_tag is None:
      if self.find is None:
        self.find_tag = Tag(None, '')
      else:
        try:
          self.find_tag = Tag(None, self.find)
        except ValueError:
          pass

class InfoSink(MatchingSink):
  def __init__(self, entry, tag, alltags):
    MatchingSink.__init__(self, tag)
    self.entry = entry
    self.alltags = alltags
    self.matching_rev = None
    self.perfect_match = 0
    self.lockinfo = { }
    self.saw_revision = False

  def define_tag(self, name, revision):
    MatchingSink.define_tag(self, name, revision)
    self.alltags[name] = revision

  def admin_completed(self):
    MatchingSink.admin_completed(self)
    if self.find_tag is None:
      # tag we're looking for doesn't exist
      if self.entry.kind == vclib.FILE:
        self.entry.absent = 1
      raise rcsparse.RCSStopParser

  def parse_completed(self):
    if not self.saw_revision:
      #self.entry.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
      self.entry.absent = 1

  def set_locker(self, rev, locker):
    self.lockinfo[rev] = locker

  def define_revision(self, revision, date, author, state, branches, next):
    self.saw_revision = True

    if self.perfect_match:
      return

    tag = self.find_tag
    rev = Revision(revision, date, author, state == "dead")
    rev.lockinfo = self.lockinfo.get(revision)

    # perfect match if revision number matches tag number or if
    # revision is on trunk and tag points to trunk.  imperfect match
    # if tag refers to a branch and either a) this revision is the
    # highest revision so far found on that branch, or b) this
    # revision is the branchpoint.
    perfect = ((rev.number == tag.number) or
               (not tag.number and len(rev.number) == 2))
    if perfect or (tag.is_branch and \
                   ((tag.number == rev.number[:-1] and
                     (not self.matching_rev or
                      rev.number > self.matching_rev.number)) or
                    (rev.number == tag.number[:-1]))):
      self.matching_rev = rev
      self.perfect_match = perfect

  def set_revision_info(self, revision, log, text):
    if self.matching_rev:
      if revision == self.matching_rev.string:
        self.entry.rev = self.matching_rev.string
        self.entry.date = self.matching_rev.date
        self.entry.author = self.matching_rev.author
        self.entry.dead = self.matching_rev.dead
        self.entry.lockinfo = self.matching_rev.lockinfo
        self.entry.absent = 0
        self.entry.log = log
        raise rcsparse.RCSStopParser
    else:
      raise rcsparse.RCSStopParser

class TreeSink(rcsparse.Sink):
  d_command = re.compile('^d(\d+)\\s(\\d+)')
  a_command = re.compile('^a(\d+)\\s(\\d+)')

  def __init__(self):
    self.revs = { }
    self.tags = { }
    self.head = None
    self.default_branch = None
    self.lockinfo = { }

  def set_head_revision(self, revision):
    self.head = revision

  def set_principal_branch(self, branch_number):
    self.default_branch = branch_number

  def set_locker(self, rev, locker):
    self.lockinfo[rev] = locker

  def define_tag(self, name, revision):
    # check !tags.has_key(tag_name)
    self.tags[name] = revision

  def define_revision(self, revision, date, author, state, branches, next):
    # check !revs.has_key(revision)
    self.revs[revision] = Revision(revision, date, author, state == "dead")

  def set_revision_info(self, revision, log, text):
    # check revs.has_key(revision)
    rev = self.revs[revision]
    rev.log = log

    changed = None
    added = 0
    deled = 0
    if self.head != revision:
      changed = 1
      lines = text.split('\n')
      idx = 0
      while idx < len(lines):
        command = lines[idx]
        dmatch = self.d_command.match(command)
        idx = idx + 1
        if dmatch:
          deled = deled + int(dmatch.group(2))
        else:
          amatch = self.a_command.match(command)
          if amatch:
            count = int(amatch.group(2))
            added = added + count
            idx = idx + count
          elif command:
            raise vclib.Error("error while parsing deltatext: %s" % command)

    if len(rev.number) == 2:
      rev.next_changed = changed and "+%i -%i" % (deled, added)
    else:
      rev.changed = changed and "+%i -%i" % (added, deled)


def _msplit(s):
  r"""Split (str) S into an array of lines.

  Only \n is a line separator. The line endings are part of the lines."""

  lines = [line + '\n' for line in s.split('\n')]
  if lines[-1] == '\n':
    del line[-1]
  else:
    lines[-1] = lines[-1][:-1]
  return lines


class StreamText:
  d_command = re.compile('^d(\d+)\\s(\\d+)\n')
  a_command = re.compile('^a(\d+)\\s(\\d+)\n')

  def __init__(self, text):
    self.text = _msplit(text)

  def command(self, cmd):
    adjust = 0
    add_lines_remaining = 0
    diffs = _msplit(cmd)
    if len(diffs) == 0:
      return
    if diffs[0] == "":
      del diffs[0]
    for command in diffs:
      if add_lines_remaining > 0:
        # Insertion lines from a prior "a" command
        # Note: Don't check if we insert a string which does not end
        # with b'\n' before an existing line. Some CVS implementation
        # can produce such edit commands.
        self.text.insert(start_line + adjust, command)
        add_lines_remaining = add_lines_remaining - 1
        adjust = adjust + 1
        continue
      dmatch = self.d_command.match(command)
      amatch = self.a_command.match(command)
      if dmatch:
        # "d" - Delete command
        start_line = int(dmatch.group(1))
        count      = int(dmatch.group(2))
        begin = start_line + adjust - 1
        del self.text[begin:begin + count]
        adjust = adjust - count
      elif amatch:
        # "a" - Add command
        start_line = int(amatch.group(1))
        count      = int(amatch.group(2))
        add_lines_remaining = count
      else:
        raise RuntimeError, 'Error parsing diff commands'

def secondnextdot(s, start):
  # find the position the second dot after the start index.
  return s.find('.', s.find('.', start) + 1)


class COSink(MatchingSink):
  def __init__(self, rev):
    MatchingSink.__init__(self, rev)

  def set_head_revision(self, revision):
    self.head = Revision(revision)
    self.last = None
    self.sstext = None

  def admin_completed(self):
    MatchingSink.admin_completed(self)
    if self.find_tag is None:
      raise vclib.InvalidRevision(self.find)

  def set_revision_info(self, revision, log, text):
    tag = self.find_tag
    rev = Revision(revision)

    if rev.number == tag.number:
      self.log = log

    depth = len(rev.number)

    if rev.number == self.head.number:
      assert self.sstext is None
      self.sstext = StreamText(text)
    elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]):
      assert len(self.last.number) == 2
      assert rev.number < self.last.number
      self.sstext.command(text)
    elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and
          (rev.number <= tag.number or len(tag.number) == depth-1)):
      assert len(rev.number) - len(self.last.number) in (0, 2)
      assert rev.number > self.last.number
      self.sstext.command(text)
    else:
      rev = None

    if rev:
      #print "tag =", tag.number, "rev =", rev.number, "<br>"
      self.last = rev
