#!/bin/sh



clr()
{
    # clr light read blabla

    local black=0
    local white=7
    local red=1
    local green=2
    local brown=3
    local blue=4
    local purple=5
    local cyan=6

    local light=""
    local color=$1
    shift

    if [ "$color" == "light" ]; then
        light="tput bold; "
        color=$1
        shift
    fi

    local code=$(eval 'echo $'$color)
    local cmd="${light}tput setaf $code"
    local color_str="$(eval "$cmd")"

    echo $color_str"$@""$(tput sgr0)"
}

shlib_init_colors()
{
    Black="$(                   tput setaf 0)"
    BlackBG="$(                 tput setab 0)"
    DarkGrey="$(     tput bold; tput setaf 0)"
    LightGrey="$(               tput setaf 7)"
    LightGreyBG="$(             tput setab 7)"
    White="$(        tput bold; tput setaf 7)"
    Red="$(                     tput setaf 1)"
    RedBG="$(                   tput setab 1)"
    LightRed="$(     tput bold; tput setaf 1)"
    Green="$(                   tput setaf 2)"
    GreenBG="$(                 tput setab 2)"
    LightGreen="$(   tput bold; tput setaf 2)"
    Brown="$(                   tput setaf 3)"
    BrownBG="$(                 tput setab 3)"
    Yellow="$(       tput bold; tput setaf 3)"
    Blue="$(                    tput setaf 4)"
    BlueBG="$(                  tput setab 4)"
    LightBlue="$(    tput bold; tput setaf 4)"
    Purple="$(                  tput setaf 5)"
    PurpleBG="$(                tput setab 5)"
    Pink="$(         tput bold; tput setaf 5)"
    Cyan="$(                    tput setaf 6)"
    CyanBG="$(                  tput setab 6)"
    LightCyan="$(    tput bold; tput setaf 6)"
    NC="$(                      tput sgr0)" # No Color
}


screen_width()
{
    local chr="${1--}"
    chr="${chr:0:1}"

    local width=$(tput cols 2||echo 80)
    width="${COLUMNS:-$width}"

    echo $width
}

hr()
{
    # generate a full screen width horizontal ruler
    local width=$(screen_width)

    printf -vl "%${width}s\n" && echo ${l// /$chr};
}

remove_color()
{
    # remove color control chars from stdin or first argument

    local sed=gsed
    which -s $sed || sed=sed

    local s="$1"
    if [ -z "$s" ]; then
        $sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g"
    else
        echo "$s" | remove_color
    fi

}

text_hr()
{
    # generate a full screen width sperator line with text.
    # text_hr "-" "a title"
    # > a title -----------------------------------------
    #
    # variable LR=l|m|r controls alignment

    local chr="$1"
    shift

    local bb="$(echo "$@" | remove_color)"
    local text_len=${#bb}

    local width=$(screen_width)
    let width=width-text_len

    local lr=${LR-m}
    case $lr in
        m)
            let left=width/2
            let right=width-left
            echo "$(printf -vl "%${left}s\n" && echo ${l// /$chr})$@$(printf -vl "%${right}s\n" && echo ${l// /$chr})"
            ;;
        r)

            echo "$(printf -vl "%${width}s\n" && echo ${l// /$chr})$@"
            ;;
        *)
            # l by default
            echo "$@$(printf -vl "%${width}s\n" && echo ${l// /$chr})"
            ;;
    esac

}



SHLIB_LOG_VERBOSE=1
SHLIB_LOG_FORMAT='[$(date +"%Y-%m-%d %H:%M:%S")] $level $title $mes'

die()
{
    err "$@" >&2
    exit 1
}
die_empty()
{
    if test -z "$1"
    then
        shift
        die empty: "$@"
    fi
}

set_verbose()
{
    SHLIB_LOG_VERBOSE=${1-1}
}

log()
{
    local color="$1"
    local title="$2"
    local level="$_LOG_LEVEL"
    shift
    shift

    local mes="$@"
    local NC="$(tput sgr0)"

    if [ -t 1 ]; then
        title="${color}${title}${NC}"
        level="${color}${level}${NC}"
    fi
    eval "echo \"$SHLIB_LOG_FORMAT\""
}
dd()
{
    debug "$@"
}
debug()
{
    if [ ".$SHLIB_LOG_VERBOSE" = ".1" ]; then
        local LightCyan="$(tput bold ; tput setaf 6)"
        _LOG_LEVEL=DEBUG log "$LightCyan" "$@" >&2
    fi
}
info()
{
    local Brown="$(tput setaf 3)"
    _LOG_LEVEL=" INFO" log "$Brown" "$@"
}
ok() {
    local Green="$(tput setaf 2)"
    _LOG_LEVEL="   OK" log "${Green}" "$@"
}
err() {
    local Red="$(tput setaf 1)"
    _LOG_LEVEL="ERROR" log "${Red}" "$@"
}

git_hash()
{
    git rev-parse $1 \
        || die "'git_hash $@'"
}
git_is_merge()
{
    test $(git cat-file -p "$1" | grep "^parent " | wc -l) -gt 1
}
git_parents()
{
    git rev-list --parents -n 1 ${1-HEAD} | { read self parents; echo $parents; }
}
git_rev_list()
{
    # --parents
    # print parent in this form:
    #     <commit> <parent-1> <parent-2> ..

    git rev-list \
        --reverse \
        --topo-order \
        --default HEAD \
        --simplify-merges \
        "$@" \
        || die "'git rev-list $@'"
}
git_tree_hash()
{
    git rev-parse "$1^{tree}"
}
git_ver()
{
    local git_version=$(git --version | awk '{print $NF}')
    local git_version_1=${git_version%%.*}
    local git_version_2=${git_version#*.}
    git_version_2=${git_version_2%%.*}

    printf "%03d%03d" $git_version_1 $git_version_2
}
git_working_root()
{
    git rev-parse --show-toplevel
}

git_rev_exist()
{
    git rev-parse --verify --quiet "$1" >/dev/null
}

git_branch_default_remote()
{
    local branchname=$1
    git config --get branch.${branchname}.remote
}
git_branch_default_upstream_ref()
{
    local branchname=$1
    git config --get branch.${branchname}.merge
}
git_branch_default_upstream()
{
    git rev-parse --abbrev-ref --symbolic-full-name "$1"@{upstream}

    # OR
    # git_branch_default_upstream_ref "$@" | sed 's/^refs\/heads\///'
}
git_branch_exist()
{
    git_rev_exist "refs/heads/$1"
}

git_head_branch()
{
    git symbolic-ref --short HEAD
}

git_commit_date()
{

    # git_commit_date author|commit <ref> [date-format]

    # by default output author-date
    local what_date="%ad"
    if [ "$1" = "commit" ]; then
        # commit date instead of author date
        what_date="%cd"
    fi
    shift

    local ref=$1
    shift

    local fmt="%Y-%m-%d %H:%M:%S"
    if [ "$#" -gt 0 ]; then
        fmt="$1"
    fi
    shift

    git log -n1 --format="$what_date" --date=format:"$fmt" "$ref"
}
git_commit_copy()
{
    # We're going to set some environment vars here, so
    # do it in a subshell to get rid of them safely later
    dd copy_commit "{$1}" "{$2}" "{$3}"
    git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
    (
    read GIT_AUTHOR_NAME
    read GIT_AUTHOR_EMAIL
    read GIT_AUTHOR_DATE
    read GIT_COMMITTER_NAME
    read GIT_COMMITTER_EMAIL
    read GIT_COMMITTER_DATE
    export  GIT_AUTHOR_NAME \
        GIT_AUTHOR_EMAIL \
        GIT_AUTHOR_DATE \
        GIT_COMMITTER_NAME \
        GIT_COMMITTER_EMAIL \
        GIT_COMMITTER_DATE

    # (echo -n "$annotate"; cat ) |

    git commit-tree "$2" $3  # reads the rest of stdin
    ) || die "Can't copy commit $1"
}

git_object_type()
{
    # $0 ref|hash
    # output "commit", "tree" etc
    git cat-file -t "$@" 2>/dev/null
}
git_object_add_by_commit_path()
{
    # add an blob or tree object to target_path in index
    # the object to add is specified by commit and path
    local target_path="$1"
    local src_commit="$2"
    local src_path="$3"

    local src_dir="$(dirname "$src_path")/"
    local src_name="$(basename "$src_path")"
    local src_treeish="$(git rev-parse "$src_commit:$src_dir")"

    git_object_add_by_tree_name "$target_path" "$src_treeish" "$src_name"

}
git_object_add_by_tree_name()
{
    # add an blob or tree object to target_path in index
    local target_path="$1"
    local src_treeish="$2"
    local src_name="$3"

    dd "arg: target_path: ($target_path) src_treeish: ($src_treeish) src_name: ($src_name)"

    local target_dir="$(dirname $target_path)/"
    local target_fn="$(basename $target_path)"
    local treeish

    if [ -z "$src_name" ] || [ "$src_name" = "." ] || [ "$src_name" = "./" ]; then
        treeish="$src_treeish"
    else
        treeish=$(git ls-tree "$src_treeish" "$src_name" | awk '{print $3}')

        if [ -z "$treeish" ]; then
            die "source treeish not found: in tree: ($src_treeish) name: ($src_name)"
        fi
    fi

    dd "hash of object to add is: $treeish"

    if [ "$(git_object_type $treeish)" = "blob" ]; then
        # the treeish imported is a file, not a dir
        # first create a wrapper tree or replace its containing tree

        dd "object to add is blob"

        local dir_treeish
        local target_dir_treeish="$(git rev-parse "HEAD:$target_dir")"
        if [ -n "target_dir_treeish" ]; then
            dir_treeish="$(git rev-parse "HEAD:$target_dir")"
            dd "target dir presents: $target_dir"

        else
            dd "target dir absent"
            dir_treeish=""
        fi

        treeish=$(git_tree_add_blob "$dir_treeish" "$target_fn" $src_treeish $src_name) || die create wrapper treeish
        target_path="$target_dir"

        dd "wrapper treeish: $treeish"
        dd "target_path set to: $target_path"
    else
        dd "object to add is tree"
    fi

    git_treeish_add_to_prefix "$target_path" "$treeish"
}

git_treeish_add_to_prefix()
{
    local target_path="$1"
    local treeish="$2"

    dd treeish content:
    git ls-tree $treeish

    git rm "$target_path" -r --cached || dd removing target "$target_path"

    if [ "$target_path" = "./" ]; then
        git read-tree "$treeish" \
            || die "read-tree $target_path $treeish"
    else
        git read-tree --prefix="$target_path" "$treeish" \
            || die "read-tree $target_path $treeish"
    fi
}

git_tree_add_tree()
{
    # output new tree hash in stdout
    # treeish can be empty
    local treeish="$1"
    local target_fn="$2"
    local item_hash="$3"
    local item_name="$4"

    {
        if [ -n "$treeish" ]; then
            git ls-tree "$treeish" \
                | fgrep -v "	$item_name"
        fi

        cat "040000 tree $item_hash	$target_fn"
    } | git mktree
}
git_tree_add_blob()
{
    # output new tree hash in stdout
    # treeish can be empty
    local treeish="$1"
    local target_fn="$2"
    local blob_treeish="$3"
    local blob_name="$4"

    {
        if [ -n "$treeish" ]; then
            git ls-tree "$treeish" \
                | fgrep -v "	$target_fn"
        fi

        git ls-tree "$blob_treeish" "$blob_name" \
            | awk -v target_fn="$target_fn" -F"	" '{print $1"	"target_fn}'
    } | git mktree
}

git_workdir_save()
{
    local index_hash=$(git write-tree)

    # add modified file to index and read index tree
    git add -u
    local working_hash=$(git write-tree)

    # restore index tree
    git read-tree $index_hash

    echo $index_hash $working_hash
}
git_workdir_load()
{
    local index_hash=$1
    local working_hash=$2

    git_object_type $index_hash || die "invalid index hash: $index_hash"
    git_object_type $working_hash || die "invalid workdir hash: $working_hash"

    # First create a temp commit to restore working tree.
    #
    # git-read-index to index and git-reset does not work because deleted file in
    # index does not apply to working tree.
    #
    # But there is an issue with this:
    #   git checkout --orphan br
    #   git_workdir_load
    # would fails, because ORIG_HEAD is not a commit.

    local working_commit=$(echo "x" | git commit-tree $working_hash) || die get working commit
    git reset --hard $working_commit || die reset to tmp commit
    git reset --soft ORIG_HEAD || die reset to ORIG_HEAD
    git read-tree $index_hash || die "load saved index tree from $index_hash"
}
git_workdir_is_clean()
{
    local untracked="$1"
    if [ "$untracked" == "untracked" ]; then
        [ -z "$(git status --porcelain)" ]
    else
        [ -z "$(git status --porcelain --untracked-files=no)" ]
    fi
}

git_copy_commit()
{
    git_commit_copy "$@"
}

git_diff_ln_new()
{
    # output changed line number of a file: <from> <end>; inclusive:
    # 27 28
    # 307 309
    # 350 350
    #
    # Usage:
    #
    #   diff working tree with HEAD:
    #       git_diff_ln_new HEAD -- <fn>
    #
    #   diff working tree with staged:
    #       git_diff_ln_new -- <fn>
    #
    #   diff staged(cached) with HEAD:
    #       git_diff_ln_new --cached -- <fn>
    #
    # in git-diff output:
    # for add lines:
    # @@ -53 +72,8
    #
    # for remove lines:
    # @@ -155 +179,0

    git diff -U0 "$@" \
        | grep '^@@' \
        | awk '{

    # @@ -155 +179,0
    # $1 $2   $3

    l = $3
    gsub("^+", "", l)

    # add default offset: ",1"
    split(l",1", x, ",")

    # inclusive line range:
    x[2] = x[1] + x[2] - 1

    # line remove format: @@ -155, +179,0
    # do need to output line range for removed.
    if (x[2] >= x[1]) {
        print x[1] " " x[2]
    }

}'
}




os_detect()
{
    local os
    case $(uname -s) in
        Linux)
            os=linux ;;
        *[bB][sS][dD])
            os=bsd ;;
        Darwin)
            os=mac ;;
        *)
            os=unix ;;
    esac
    echo $os
}

mac_ac_power_connection()
{
    #  Connected: (Yes|No)
    system_profiler SPPowerDataType \
        | sed '1,/^ *AC Charger Information:/d' \
        | grep Connected:
}


mac_power()
{

    # $0 is-battery          exit code 0 if using battery.
    # $0 is-ac-power         exit code 0 if using ac power.

    local cmd="$1"
    local os=$(os_detect)

    if [ "$os" != "mac" ]; then
        err "not mac but: $os"
        return 1
    fi

    case $cmd in

        is-battery)
            mac_ac_power_connection | grep -q No
            ;;

        is-ac-power)
            mac_ac_power_connection | grep -q Yes
            ;;

        *)
            err "invalid cmd: $cmd"
            return 1
            ;;
    esac
}

fn_match()
{
    # $0 a.txt *.txt
    case "$1" in
        $2)
            return 0
            ;;
    esac
    return 1
}

main()
{
    local cmd="${1-update}"
    local match="$2"

    case "$cmd" in
        -h|--help)
            usage
            exit 0
            ;;
        init)
            init_config
            exit 0
            ;;
    esac


    local root=$(git_working_root)
    [ "x$root" = "x" ] && die 'looking for git repo root'

    local conf_fn=./.gitsubrepo

    cd "$root"

    [ -f .gitsubrepo_refs ] && rm .gitsubrepo_refs
    > .gitsubrepo_refs

    local base=
    local remote=
    local remote_suffix=
    local prefix=
    local url=
    local ref=
    local localtag=
    while read a b c d; do

        if [ "x$a" = "x" ] || [ "${a:0:1}" = "#" ]; then
            continue
        fi

        # [ base: xp/vim-d ]
        # declare base dir for following sub repo
        #
        # [ remote: https://github.com/ ]
        # declare remote base url for following sub repo
        if test "${a:0:1}" = '['
        then

            case $b in
                base:)
                    base="$c"
                    base="${base%%]}"
                    base="${base%/}/"
                    if test "$base" = "/"; then
                        base=
                    fi
                    ;;
                remote:)
                    remote="$c"
                    ;;
                remote_suffix:)
                    remote_suffix="$c"
                    ;;
                *)
                    echo "invalid tag: $b"
                    exit 1
            esac
            continue
        fi

        prefix="$a"
        url="$b"
        ref="$c"
        dir="$d"

        if [ "x$match" != "x" ] && ! fn_match "$prefix" *$match*; then
            continue
        fi

        if [ ".$cmd" = ".add" ]; then
            if [ -d "$prefix" ]; then
                continue
            fi
        fi

        # "xpt drmingdrmer/${prefix} master" is translated to "xpt drmingdrmer/xpt master"
        url=$(eval "echo $url")

        if test "${url%/}" != "$url"; then
            url="${url}$prefix"
        fi

        if test "${url:0:8}" != "https://" && test "${url:0:4}" != "git@"
        then
            url="${remote}${url}${remote_suffix}"
        fi

        fetch_remote "$base$prefix" "$url" "$ref" "$dir" &

    done<"$conf_fn"
    wait

    while read prefix url ref localtag dir; do

        dd "$prefix, $url, $ref, $localtag, $dir"


        local tag_hash=$(git_hash "$localtag") || die get hash of $localtag
        local tag_author_date=$(git_commit_date author "$localtag") || die get authoer date of $localtag
        local head_hash=$(git_hash HEAD)

        dd "tag_hash:   $tag_hash"
        dd "head_hash:  $head_hash"

        # clear old $prefix
        if [ -d "$prefix" ]; then
            git rm -r "$prefix" >/dev/null || die rm "$prefix"
        fi

        dd "add to ($prefix): from ($dir)"
        git_object_add_by_commit_path "$prefix" "$tag_hash" "$dir" || die git_object_add_by_commit_path

        # remove not in index files
        git checkout -- "$prefix" || die "checkout $prefix"

        # get tree object
        local tree=$(git write-tree) || die git write-tree
        dd "tree:       $tree"

        if [ "$tree" = "$(git_tree_hash HEAD)" ]; then
            info "Nothing changed: $prefix"
            continue
        fi

        local changes=
        local latest_hash=$(find_latest_squash "$prefix") || die find_latest_squash of "$prefix"

        if [ -n "$latest_hash" ]; then
            info "latest commit of $prefix: $latest_hash ($(git_commit_date author "$latest_hash"))"
            # add 4 space indent
            changes="$(git --no-pager shortlog $latest_hash..$tag_hash | awk '{print "    "$0}')"
        else
            info "latest commit of $prefix: not found"
        fi

        local commit=$(
        {
            cat <<-END
		Squashed $prefix $ref:$dir ${tag_hash:0:9} (${tag_author_date})

		url:      $url
		ref:      $ref
		sub-dir:  $dir

		localtag: $localtag $tag_hash

		git-subrepo-dir: $prefix
		git-subrepo-hash: $tag_hash

		changes: from ${latest_hash:0:9} ($(git_commit_date author "$latest_hash"))
		$changes
		END
        } | git commit-tree $tree -p $head_hash
        ) || die commit
        dd "commit:     $commit"

        git merge --ff-only --no-edit --commit $commit

    done<".gitsubrepo_refs"

    while read a b c localtag dir; do
        dd $a, $b, $c, $localtag, $dir
        git update-ref -d $localtag
    done<".gitsubrepo_refs"

    rm .gitsubrepo_refs
}

init_config()
{
    local root=$(git_working_root)
    [ "x$root" = "x" ] && die 'looking for git repo root'

    (
    cd $root

    if [ -f .gitsubrepo ]; then
        echo ".gitsubrepo already exists"
        return 0
    fi

    cat >.gitsubrepo <<-END
	[ remote_suffix: .git ]
	[ remote: https://github.com/ ]

	[ base: ]
	git-subrepo      baishancloud/git-subrepo       master   git-subrepo

	[ base: deps ]
	pykit            baishancloud/pykit             master


	# git-subrepo
	#   for maintaining sub git repo
	#   https://github.com/baishancloud/git-subrepo
	END

    vim .gitsubrepo
    git add .gitsubrepo
    echo "git commit to persistent .gitsubrepo config"
    )
}

find_latest_squash()
{
    # learned from git-subtree: find_latest_squash

    git log --grep="^Squashed $prefix " \
        --pretty=format:'START %H%n%b%nEND%n' HEAD |
    while read a b junk; do
        case $a in
            git-subrepo-hash:)
                echo $b
                return 0
                ;;
            esac
    done
}

fetch_remote()
{
    local prefix="$1"
    local url="$2"
    local ref="$3"
    local dir="$4"

    local localtag=refs/tags/_gitsubrepo/$prefix
    # git tag does not allow leading dot. E.g.: "refs/tags/_gitsubrepo/a/.b"
    localtag="${localtag//./-}"

    local mes="$url $ref -> $localtag"

    dd "Start fetching $mes"
    git fetch --no-tags "$url" "$ref:$localtag" || die fetch "$mes"
    echo "$prefix $url $ref $localtag" "$dir" >> .gitsubrepo_refs
    dd "Done fetching $mes"
}

usage()
{
    cat <<-END
usage: git subrepo

    Merge sub git repo into sub-directory in a parent git dir with git-submerge.
    git-subrepo reads config from ".gitsubrepo" resides in the root of parent
    git working dir.

    Configure file ".gitsubrepo" syntax:


        # set base of remote url to "https://github.com/"
        [ remote: https://github.com/ ]


        # set base of local dir to "plugin"
        [ base: plugin ]


        # <local dir>   <remote uri>            <ref to fetch>    [<dir>]
        gutter          airblade/vim-gitgutter  master            src


        # if <remote uri> ends with "/", <local dir> will be added after "/"
        ansible-vim     DavidWittman/           master


        # change base to "root"
        [ base: ]


        # use a specific commit 1a2b3c4
        ultisnips       SirVer/                 1a2b3c4


        # add a sub-directory
        ultisnips       SirVer/                 1a2b3c4           src


    With above config, "git subrepo" will:

    -   fetch master of https://github.com/DavidWittman/ansible-vim
        and put it in:
            <git-root>/plugin/ansible-vim

    -   fetch master of https://github.com/airblade/vim-gitgutter
        and put it in:
            <git-root>/plugin/gutter

    -   fetch commit 1a2b3c4 of https://github.com/SirVer/ultisnips
        and put it in:
            <git-root>/ultisnips

END
}

main "$@"
