#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you under the Apache License, Version 2.0 (the
#       "License"); you may not use this file except in compliance
#       with the License.  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing,
#       software distributed under the License is distributed on an
#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#       KIND, either express or implied.  See the License for the
#       specific language governing permissions and limitations
#       under the License.

import re
i
mport os
import shutil
import string
import logging
import subprocess
import time
import operator as op
from subprocess import Popen, PIPE
from hashlib import sha1
from cStringIO import StringIO
from datetime import datetime
import tempfile
from shutil import rmtree

import tg
import pysvn
from paste.deploy.converters import asbool, asint
from pymongo.errors import DuplicateKeyError
from pylons import tmpl_context as c, app_globals as g

from ming.base import Object
from ming.orm import Mapper, FieldP
roperty
from ming.utils import LazyProperty

from allura import model as M
from allura.lib import helpers as h
from allura.model.auth import User
from allura.model.repository import zipdir
from allura.model import repository as RM

log = logging.getLogger(__name__)


class Repository(M.Repository):
    tool_name = 'SVN'
    repo_id = 'svn'
    type_s = 'SVN Repository'

    class __mongometa__:
        name = 'svn-repository'
    branches = FieldProperty([dict(name=str, object_id=str)])
    _refresh_precompute = False

    @LazyProperty
    def _impl(self):
        return SVNImplementation(self)

    def clone_command(self, category, username=''):
        '''Return a string suitable for copy/paste that would clone this repo locally
           category is one of 'ro' (read-only), 'rw' (read/write), or 'https' (read/write via https)
        '''
        if not username and c.user not in (None, User.anonymous()):
            username = c.user.username
        tpl = string.Template(tg.config.get('scm.clone.%s.%s' % (category, self.tool)) or
                              tg.config.get('scm.clone.%s' % self.tool))
        return tpl.substitute(dict(username=username,
                                   source_url=self.clone_url(
                                       category, username) + c.app.config.options.get(
                                       'checkout_url'),
                                   dest_path=self.suggested_clone_dest_path()))

    def latest(self, branch=None):
        if self._impl is None:
            return None
        return self._impl.commit('HEAD')

    def tarball_filename(self, revision, path=None):
        fn = super(Repository, self).tarball_filename(revision, path)
        path = self._impl._path_to_root(path, revision)
        fn += ('-' + '-'.join(path.split('/'))) if path else ''
        return fn

    def rev_to_commit_id(self, rev):
        return self._impl.rev_parse(rev)


class SVNCalledProcessError(Exception):

    def __init__(self, cmd, returncode, stdout, stderr):
        self.cmd = cmd
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr

    def __str__(self):
        return "Command: '%s' returned non-zero exit status %s\nSTDOUT: %s\nSTDERR: %s" % \
            (self.cmd, self.returncode, self.stdout, self.stderr)


def svn_path_exists(path, rev=None):
    svn = SVNLibWrapper(pysvn.Client())
    if rev:
        rev = pysvn.Revision(pysvn.opt_revision_kind.number, rev)
    else:
        rev = pysvn.Revision(pysvn.opt_revision_kind.head)
    try:
        svn.info2(path, revision=rev, recurse=False)
        return True
    except pysvn.ClientError:
        return False


class SVNLibWrapper(object):

    """Wrapper around pysvn, used for instrumentation."""

    def __init__(self, client):
        self.client = client

    def checkout(self, *args, **kw):
        return self.client.checkout(*args, **kw)

    def add(self, *args, **kw):
        return self.client.add(*args, **kw)

    def checkin(self, *args, **kw):
        return self.client.checkin(*args, **kw)

    def info2(self, *args, **kw):
        return self.client.info2(*args, **kw)

    def log(self, *args, **kw):
        return self.client.log(*args, **kw)

    def cat(self, *args, **kw):
        return self.client.cat(*args, **kw)

    def list(self, *args, **kw):
        return self.client.list(*args, **kw)

    def __getattr__(self, name):
        return getattr(self.client, name)


class SVNImplementation(M.RepositoryImplementation):
    post_receive_template = string.Template(
        '#!/bin/bash\n'
        '# The following is required for site integration, do not remove/modify.\n'
        '# Place user hook code in post-commit-user and it will be called from here.\n'
        'curl -s $url\n'
        '\n'
        'DIR="$$(dirname "$${BASH_SOURCE[0]}")"\n'
        'if [ -x $$DIR/post-commit-user ]; then'
        '  exec $$DIR/post-commit-user "$$@"\n'
        'fi')

    def __init__(self, repo):
        self._repo = repo

    @LazyProperty
    def _svn(self):
        return SVNLibWrapper(pysvn.Client())

    @LazyProperty
    def _url(self):
        return 'file://%s%s' % (self._repo.fs_path, self._repo.name)

    def shorthand_for_commit(self, oid):
        return '[r%d]' % self._revno(self.rev_parse(oid))

    def url_for_commit(self, commit, url_type=None):
        if hasattr(commit, '_id'):
            object_id = commit._id
        elif commit == self._repo.app.default_branch_name:
            object_id = commit
        else:
            object_id = self.rev_parse(commit)
        if ':' in object_id:
            object_id = str(self._revno(object_id))
        return os.path.join(self._repo.url(), object_id) + '/'

    def init(self, default_dirs=False, skip_special_files=False):
        fullname = self._setup_paths()
        log.info('svn init %s', fullname)
        if os.path.exists(fullname):
            shutil.rmtree(fullname)
        subprocess.call(['svnadmin', 'create', self._repo.name],
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        cwd=self._repo.fs_path)
        if not skip_special_files:
            self._setup_special_files()
        self._repo.set_status('ready')
        # make first commit with dir structure
        if default_dirs:
            tmp_working_dir = tempfile.mkdtemp(prefix='allura-svn-r1-',
                                               dir=tg.config.get('scm.svn.tmpdir', g.tmpdir))
            log.info('tmp dir = %s', tmp_working_dir)
            self._repo._impl._svn.checkout(
                'file://' + fullname, tmp_working_dir)
            os.mkdir(tmp_working_dir + '/trunk')
            os.mkdir(tmp_working_dir + '/tags')
            os.mkdir(tmp_working_dir + '/branches')
            self._repo._impl._svn.add(tmp_working_dir + '/trunk')
            self._repo._impl._svn.add(tmp_working_dir + '/tags')
            self._repo._impl._svn.add(tmp_working_dir + '/branches')
            self._repo._impl._svn.checkin([tmp_working_dir + '/trunk',
                                           tmp_working_dir + '/tags',
                                           tmp_working_dir + '/branches'],
                                          'Initial commit')
            shutil.rmtree(tmp_working_dir)
            log.info('deleted %s', tmp_working_dir)

    def can_hotcopy(self, source_url):
        if not (asbool(tg.config.get('scm.svn.hotcopy', True)) and
                source_url.startswith('file://')):
            return False
        # check for svn version 1.7 or later
        stdout, stderr, returncode = self.check_call(['svn', '--version'])
        pattern = r'version (?P<maj>\