#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
cot

CotEditor
http://coteditor.com

Created by 1024jp on 2015-08-12.

------------------------------------------------------------------------------

© 2015-2016 1024jp

Licensed 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.

"""

from __future__ import print_function

import argparse
import errno
import fcntl
import os
import sys
import time
from subprocess import Popen, PIPE, CalledProcessError


# meta data
__version__ = '2.4.1'
__description__ = 'command-line utility for CotEditor.'


# constants
APPLICATION_NAME = 'CotEditor'
WAIT_INTERVAL = 0.5


# set encoding for file path
if sys.version_info < (3, 0):
    reload(sys)
    sys.setdefaultencoding('utf8')


# MARK: Bundle

def bundle_path():
    """Return application path if this script is bundled in an OS X application.

    Returns:
        path (str): Path to .app directory or None if not found.
    """
    path = os.path.realpath(__file__)

    # find '.app' extension
    while path is not '/':
        path = os.path.dirname(path)
        _, extension = os.path.splitext(path)
        if extension == '.app':
            return path

    return None


# MARK: OSA Script

def run_osascript(script, is_async=False):
    """Run osascript.

    Args:
        script (str): Osascript.
        is_async (bool): If need to wait for finish.
    Returns:
        result (str): Return value of the script.
    """
    if is_async:
        script = 'ignoring application responses\n' + script + '\nend ignoring'
    p = Popen(['osascript', '-'], stdin=PIPE, stdout=PIPE, stderr=PIPE)
    stdout, _ = p.communicate(script.encode('utf-8'))
    if p.returncode:
        raise CalledProcessError(p.returncode, script)

    result = stdout.decode('utf-8')

    # strip the last line ending
    #   -> Don't use `rstrip` since it removes multiple line endings.
    if result.endswith('\n'):
        result = result[:-1]

    return result


class ScriptableApplication(object):
    """OSA-Scriptable OS X application object.
    """
    _pid = None

    def __init__(self, name):
        self.name = name
        self.bundle_identifier = run_osascript('id of app "{}"'.format(name))

    def tell(self, script, is_async=False):
        """Tell OSA command to the application.

        Args:
            script (str): OSA command
        Returns:
            result (str): Return value of the script.
        """
        script = 'tell app "{}" to {}'.format(self.name, script)
        return run_osascript(script, is_async)

    def launch(self, background=False):
        """Launch application.

        Args:
            background (bool): Open in background?
        """
        if background:
            self.tell('run')
        else:
            self.tell('activate')

        # get unix id
        system_events = ScriptableApplication("System Events")
        script = ('unix id of processes whose bundle identifier is "{}"'
                  .format(self.bundle_identifier))
        self._pid = system_events.tell(script)

    def open(self, path):
        """Open given file path in the application.

        Args:
            path (str): Path to file.
        """
        path = path.replace('"', '\\"')
        self.tell('open POSIX file "{}"'.format(path), is_async=True)
        # -> Opening a file that is already opened in the application tekes
        #    somehow extremely long time. So, we don't wait.

    def tell_document(self, script, index=1):
        """Tell OSA command to a document of the application.

        Args:
            script (str): OSA command
            index (int): Index number of the document to handle (1-based).
        Returns:
            result (str): Return value of the script.
        """
        return self.tell('tell document {} to {}'.format(index, script))

    def window_id(self, index=1):
        """Get window identifier.

        Args:
            index (int): Index number of the window to get id (1-based).
        Returns:
            window_id (str): Identifier of document's window opened.
        """
        return self.tell('id of window {}'.format(index))

    def window_exists(self, window_id):
        """Check if window exists.

        Args:
            window_id (str): identifier of window to check existance.
        Returns:
            result (bool): Window exists?
        """
        script = '(first window whose id is {}) is visible'.format(window_id)
        result = None
        try:
            result = self.tell(script)
        except CalledProcessError:
            pass
        return result == 'true'


# MARK: Args Parse

def parse_args():
    """Parse command line arguments.

    Returns:
        Parsed args object.
    """
    # create parser instance
    parser = argparse.ArgumentParser(description=__description__)

    # set positional argument
    parser.add_argument('file',
                        type=str,
                        metavar='FILE',
                        nargs='?',
                        help="path to file to open"
                        )

    # set optional arguments
    parser.add_argument('-v', '--version',
                        action='version',
                        version=__version__
                        )
    parser.add_argument('-w', '--wait',
                        action='store_true',
                        default=False,
                        help="wait for opened file to be closed"
                        )
    parser.add_argument('-g', '--background',
                        action='store_true',
                        default=False,
                        help="do not bring the application to the foreground"
                        )
    parser.add_argument('-n', '--new',
                        action='store_true',
                        default=False,
                        help="create a new blank document"
                        )
    parser.add_argument('-l', '--line',
                        type=int,
                        help="jump to specific line in opened document"
                        )
    parser.add_argument('-c', '--column',
                        type=int,
                        help="jump to specific column in opened document"
                        )

    args = parser.parse_args()

    # check file existance and create if needed
    if args.file:
        open_mode = 'r'
        if args.new and not os.path.exists(args.file):
            open_mode = 'w'   # overwrite mode to create new file
            # create directory if not exists yet
            dirpath = os.path.dirname(args.file)
            if dirpath:
                try:
                    os.makedirs(dirpath)
                except OSError as err:  # guard against race condition
                    if err.errno != errno.EEXIST:
                        parser.error("argument FILE: {}".format(err))
        # check readability or create new one
        try:
            open(args.file, open_mode).close()
        except IOError as err:
            parser.error("argument FILE: {}".format(err))

    return args


# MARK: - Main

def main(args, stdin):
    app_identifier = bundle_path() or APPLICATION_NAME
    app = ScriptableApplication(app_identifier)

    # launch
    app.launch(background=args.background)

    # create document
    if args.file:
        # open file
        path = os.path.realpath(args.file)
        app.open(path)

    elif stdin:
        # new document with piped text
        sanitized_stdin = stdin.replace('"', '\\"')
        app.tell('make new document')
        app.tell_document('set contents to "{}"'.format(sanitized_stdin))
        app.tell_document('set range of selection to {0, 0}')

    elif args.new:
        # new blank document
        app.tell('make new document')

    if app.tell('number of documents') is '0':
        return

    # jump to location
    if args.line is not None or args.column is not None:
        contents = app.tell_document('contents')
        lines = contents.splitlines(True)  # turn on keepends flag

        # sanitize line number
        line = args.line or 1  # line number starts with 1
        if line == 0:
            line = 1
        elif line < 0:  # negative line number counts from the last line
            line_count = len(lines)
            line = max(1, line_count - abs(line) + 1)

        # count location of line head
        location = 0
        for a_line in lines[0:line - 1]:
            location += len(a_line)

        # sanitize and add column position
        if args.column is not None:
            last_line = lines[line - 1]
            column = min(args.column, len(last_line))
            location += column

        # set selection range
        app.tell_document('set range of selection to {{{}, 0}}'.format(
            location))

        # jump
        app.tell_document('scroll to caret')

    # wait for window close
    if args.wait and (args.file or stdin or args.new):
        window_id = app.window_id()
        while app.window_exists(window_id):
            time.sleep(WAIT_INTERVAL)


if __name__ == "__main__":
    # parse arguments
    args = parse_args()

    # read piped text if exists
    fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)  # non-block
    try:
        stdin = sys.stdin.read()
    except:
        stdin = None

    main(args, stdin)
