#!/usr/bin/env python2.6

"""
Neat little command-line JSON tool written by Alexander Boyd as part of the AFN
project. Copyright 2012 Alexander Boyd (alex@opengroove.org). Released under
the terms of the GNU Lesser General Public License.
"""

import sys
from os.path import join, dirname, realpath, exists as os_path_exists
afn_path = dirname(realpath(__file__))
# If we're running in the AFN repository, add the source folder to the path.
if os_path_exists(join(afn_path, "src")):
    sys.path.append(join(afn_path, "src"))

from afn.backports.argparse import ArgumentParser ###REPLACE:src/afn/backports/argparse.py
from afn.utils.argparseutils import AppendWithConst ###REPLACE:src/afn/backports/argparse.py
from functools import partial
import json
import plistlib

description = """
A command-line tool for editing JSON data and p-list files.

Like ffmpeg, the order of command line arguments is significant. Every argument
except for -S and --help is an action to be performed. Actions are performed in
the order that they appear in the command line arguments, and a single action
can be used multiple times.

There are three important concepts that json uses to allow modification of data:
the root value, the current path, and the current mode.

The root value is the data that you're editing with the json command. When
you're using the --file command, for example, the root value is the data
contained within that file.

The current path is a path within the root value. This is analogous to the
working directory of a shell such as bash, except that the current path relates
to JSON objects/dictionaries and lists/arrays, not files and directories. There
are a number of commands analogous to cd, mkdir, and such for modifying the
current path. Note that unlike shell commands such as cd, the current path can
be changed to a path that doesn't actually exist; modifying such a path will
cause it to be created automatically, as if the equivalent of "mkdir" had been
run for you.

The current mode is the file format that will be used to read and write data.
It can be changed in the middle of a json command line, which can be used to,
for example, read data in one file format and write data in another.

The current mode is, by default, JSON mode. This can be changed to p-list mode
(a mode where data is read and written as Apple p-list files) with -p. It can
also be changed to Python mode (a mode where data is read and written as Python
objects) with -P, and it can be changed to text mode (a mode where strings,
numbers, and such are written as plain text without character escapes or quotes)
with -t. -j changes the mode back to JSON mode.

Some commands, such as --key, have variants with "-and-up" appended to the
command name. These cause a --up to be added after the command immediately
following the "-and-up" command. For example, to create a new object and set
the keys "first", "second", and "third" to have the values "one", "two", and
"three", respectively, without using "-and-up" commands, you would have to do
something like:

    json -o -k first -s one -u -k second -s two -u -k third -s three -u

but with "-and-up" commands (-K is the -and-up equivalent of -k), you could do:

    json -o -K first -s one -K second -s two -K third -s three

which is quite a bit more intuitive.

Examples of how to use this command-line utility are present at the end of this
help output.
"""

epilog = """
Examples:
  Examples will be coming soon.

Author:
  Alexander Boyd (alex@opengroove.org). Created as part of the AFN project.
"""

parser = ArgumentParser(add_help=False, description="!!!DESC!!!", epilog="!!!END!!!")
parser.add_argument("-S", "--suppress-warnings", dest="warnings", action="store_false",
                    help="Suppress warnings about some potentially-pathological situations")
parser.add_argument("-V", "--verbose", dest="verbose", action="store_true",
                    help="Print information about the steps this command is taking as it takes them (TODO: add this)")
parser.add_argument("-?", "--help", action='store_true', help='show this help message and exit')

path_actions = parser.add_argument_group("path-related actions")
add_path_action = partial(path_actions.add_argument, dest="actions", action=AppendWithConst)
add_path_action("-u", "--up", const="up", nargs=0,
        help='Jumps one level up the path. Analogous to "cd ..".')
add_path_action("-k", "--key", const="key", nargs=1, metavar="<key>")
add_path_action("-K", "--key-and-up", const="key-and-up", nargs=1, metavar="<key>",
        help='Adds the specified key onto the end of the path. Analogous to "cd <key>".')
add_path_action("-i", "--index", const="index", nargs=1, metavar="<index>")
add_path_action("-I", "--index-and-up", const="index-and-up", nargs=1, metavar="<index>",
        help='Adds the specified index onto the end of the path. Analogous to "cd <index>".')
add_path_action("-m", "--make", const="make", nargs=0,
        help="This action doesn't work yet. In the future, it will function analogously "
        'to "mkdir -p".')

io_actions = parser.add_argument_group("I/O actions")
add_io_action = partial(io_actions.add_argument, dest="actions", action=AppendWithConst)
add_io_action("-r", "--read", const="read", nargs=0,
        help="Reads data from stdin using the current mode, then sets the value at the "
        "current path to contain the data read.")
add_io_action("-R", "--read-file", const="read-file", nargs=1, metavar="<file>",
        help="Reads the specified file using the current mode, then sets the value at the "
        "current path to contain the data read.")
add_io_action("-w", "--write", const="write", nargs=0,
        help="Writes the root value to stdout using the current mode. Note that this writes "
        "the root value, not the value at the current path.")
add_io_action("-W", "--write-file", const="write-file", nargs=1, metavar="<file>",
        help="Writes the root value to the specified file using the current mode. Note that "
        "this writes the root value, not the value at the current path.")
add_io_action("-c", "--write-current", const="write-current", nargs=0,
        help="Writes the value at the current path to stdout using the current mode.")
add_io_action("-C", "--write-current-to-file", const="write-current-to-file", nargs=1, metavar="<file>",
        help="Writes the value at the current path to the specified file using the current mode.")
add_io_action("-f", "--file", const="file", nargs=1, metavar="<file>",
        help="Same as -R <file>, but adds -W <file> to the end of the command line. "
        "This allows for editing files in place without having to specify -R and -W separately. "
        "Note that even if the mode is changed after -f is specified, the file will still be "
        "written back using the mode that -f used to read the file.")

addition_actions = parser.add_argument_group("data actions")
add_addition_action = partial(addition_actions.add_argument, dest="actions", action=AppendWithConst)
add_addition_action("-o", "--object", const="object", nargs=0,
        help="Sets the value at the current path to be a newly-created object/dictionary.")
add_addition_action("-l", "--list", const="list", nargs=0,
        help="Sets the value at the current path to be a newly-created list/array.")
add_addition_action("-s", "--string", const="string", nargs=1, metavar="<text>",
        help="Sets the value at the current path to be a string containing the specified text.")
add_addition_action("--data", const="data", nargs=1, metavar="<raw-data>",
        help="Sets the value at the specified path to be a data value containing the specified raw data.")
add_addition_action("--data-hex", const="data-hex", nargs=1, metavar="<hex-data>",
        help="Sets the value at the specified path to be a data value containing the specified data, which should "
        "be encoded in hexidecimal.")
add_addition_action("--data-base64", const="data-base64", nargs=1, metavar="<base64-data>",
        help="Sets the value at the specified path to be a data value containing the speicifed data, which should "
        "be encoded in Base64.")
add_addition_action("-n", "--number", const="number", nargs=1, metavar="<number>",
        help="Sets the value at the current path to be the specified number.")
add_addition_action("--date", const="date", nargs=1, metavar="<seconds-since-epoch>",
        help="Sets the value at the current path to be a date representing the specified number of seconds since "
        "Midnight, 1 January 1970, UTC.")
add_addition_action("--strptime", const="strptime", nargs=2, metavar=("<format>", "<date>"),
        help="Parses the specified date using the specified format, then sets the value at the "
        "current path to be the corresponding date. <format> uses the same format as the C "
        'strptime function (the UNIX date command also uses the same format); see "man strptime" '
        ' or "man date" for details of the format used.')
add_addition_action("-b", "--bool", "--boolean", const="bool", nargs=1, metavar="<bool>",
        help="Sets the value at the current path to be the specified boolean, which can be "
        "one of true, false, yes, no, on, off, t, f, y, n, 1, or 0. The value is case-insensitive.")
add_addition_action("-T", "--true", const="true", nargs=0,
        help="Sets the value at the current path to the boolean true.")
add_addition_action("-F", "--false", const="false", nargs=0,
        help="Sets the value at the current path to the boolean false.")
add_addition_action("-N", "--null", const="null", nargs=0,
        help="Sets the value at the current path to null.")
add_addition_action("-v", "--value", const="value", nargs=1, metavar="<value>",
        help="Reads <value> using the current mode and sets the value at the current "
        "path to it.")

mode_actions = parser.add_argument_group("modes")
add_mode_action = partial(mode_actions.add_argument, dest="actions", action=AppendWithConst)
add_mode_action("-j", "--json", const="json", nargs=0,
        help="Changes the current mode to JSON mode. Data will be read and written "
        "as JSON values.")
add_mode_action("-p", "--plist", const="plist", nargs=0,
        help="Changes the current mode to p-list mode. Data will be read and written "
        "using Apple's p-list XML file format. Support for binary plists will be coming soon.")
add_mode_action("-P", "--python", const="python", nargs=0,
        help="Changes the current mode to Python mode. Data will be read and written "
        "as Python objects. WARNING: This is not safe; reading data in Python mode "
        "allows evaluation of arbitrary Python expressions, so Python mode should "
        "only be used when you know that the data you'll be reading is safe.")
add_mode_action("-t", "--text", const="text", nargs=0,
        help="Changes the current mode to text mode. Data will be read as one "
        "continuous string, and only strings and numbers can be written with this "
        "mode. Strings will be written without quotes or escapes. This is intended "
        "to be used when you want to print out the value of "
        "a string without quotes, escapes, and other format-specific things.")

modify_actions = parser.add_argument_group("modification actions")
add_modify_action = partial(modify_actions.add_argument, dest="actions", action=AppendWithConst)
add_modify_action("-x", "--delete", "--remove", const="delete", nargs=0,
        help="Deletes the value at the current path. For example, if the current "
        'value is an object, and you want to delete the keys "example" and "test", '
        'you could do that with -K example -d -u -K test -d.')
add_modify_action("-a", "--append", const="append", nargs=0)
add_modify_action("-A", "--append-and-up", const="append-and-up", nargs=0,
        help="Appends a new item to the list/array that the current path points to, "
        'then adds the index of the newly-created item to the path. The item will have '
        'the value null; this can then be changed with a command such as -s or -n. For '
        'example, "-a -n 5" will append the number 5 to the list/array at the current path.')
add_modify_action("-g", "--insert", const="insert", nargs=1, metavar="<index>")
add_modify_action("-G", "--insert-and-up", const="insert-and-up", nargs=1, metavar="<index>",
        help="Inserts a new item into the list/array that the current path points to, "
        'then adds the specified index onto the path. Indexes start at 0. For example, running '
        '-g 0 -n 7 into the list [1,2,3] will change the list to [7,1,2,3], and running '
        '-g 1 -n 7 on the list [1,2,3] will change the list to [1,7,2,3]. This is different '
        'from -i, which will just modify an existing item; running -i 1 -n 7 on the list '
        '[1,2,3] will change the list to [1,7,3].')

output_actions = parser.add_argument_group("output actions")
add_output_action = partial(output_actions.add_argument, dest="actions", action=AppendWithConst)
add_output_action("--size", const="size", nargs=0,
        help="Prints to stdout the number of entries or items in the current list or dictionary, the number of "
        "characters in the current string, or the number of bytes in the current data value.")
add_output_action("--type", const="type", nargs=0,
        help="Prints to stdout the type of the value at the current path. This will be one of number, boolean, null, "
        "string, list, dictionary, date, or data.")
add_output_action("--keys", const="keys", nargs=0,
        help="Prints to stdout the keys in the current dictionary, each on a new line.")
add_output_action("--print", const="print", nargs=0,
        help="Prints to stdout the value at the current path, without any formatting. If the value is a "
        "number, boolean, or if the value is null, it will be printed as it would using the JSON format. "
        "If the value is a string, its textual content will be printed, without escapes. If the value is "
        "data, it will be printed byte-for-byte with no escaping. It is an error to try to print a list or "
        "dictionary with this command.")
add_output_action("--print-hex", const="print-hex", nargs=0,
        help="Prints the value at the current path, which should be data, to stdout, encoded using hexidecimal.")
add_output_action("--print-base64", const="print-base64", nargs=0,
        help="Prints the value at the current path, which should be data, to stdout, encoded using Base64.")
add_output_action("--print-rot13", const="print-rot13", nargs=0,
        help="Prints the value at the current path, which should be a string, to stdout, encoded "
        "using ROT-13.")
add_output_action("--print-flipped", const="print-flipped", nargs=0,
        help="Prints the value at the current path, which should be a string, to stdout, flipped such that one "
        "would be able to read the text in question upside-down.")
add_output_action("--strftime", const="strftime", nargs=1, metavar="<format>",
        help="Formats the date at the current path using <format> and prints the result to stdout."
        "<format> uses the same format as the C "
        'strftime function (the UNIX date command also uses the same format); see "man strftime" '
        ' or "man date" for details of the format used.')



flip_chars = (u"a\u0250bqc\u0254dpe\u01ddf\u025fg\u0183h\u0265i\u0131j\u027ek\u029ellm\u026fnuoopd"
		u"qbr\u0279sst\u0287unv\u028cw\u028dxxy\u028ezz.\u02d9,'',`,;\u061b!\u00a1?\u00bf[]][())({}}{<>><_\u203e")
flip_charmap = dict(zip(flip_chars[0::2], flip_chars[1::2])) # Map with odd-numbered
# chars in flip_chars as keys and even-numbered chars as values
flipped = lambda text: "".join([flip_charmap.get(c, c) for c in reversed(text.lower())])


class JSONFormatter(object):
    def read(self, text):
        return json.loads(text)
    def read_file(self, filename):
        with open(filename, "r") as f:
            return json.load(f) 
    def write(self, value):
        return json.dumps(value)
    def write_file(self, filename, value):
        with open(filename, "w") as f:
            json.dump(value, f)

class PlistFormatter(object):
    def read(self, text):
        return plistlib.readPlistFromString(text)
    def read_file(self, filename):
        return plistlib.readPlist(filename)
    def write(self, value):
        return plistlib.writePlistToString(value)
    def write_file(self, filename, value):
        plistlib.writePlist(value, filename)

class PythonFormatter(object):
    def read(self, text):
        return eval(text)
    def read_file(self, filename):
        with open(filename, "r") as f:
            return eval(f.read())
    def write(self, value):
        return repr(value)
    def write_file(self, filename, value):
        with open(filename, "w") as f:
            f.write(repr(value))

class TextFormatter(object):
    def read(self, text):
        return text
    def read_file(self, filename):
        with open(filename, "r") as f:
            return f.read()
    def write(self, value):
        return str(value)
    def write_file(self, filename, value):
        with open(filename, "w") as f:
            f.write(str(value))

def lookup(parent=False, create=False):
    if create and not parent:
        raise Exception("Parent must be true if create is true")
    global formatter, path, working
    value = working
    if parent:
        lookup_path = path[:-1]
    else:
        lookup_path = path
    for i, p in enumerate(lookup_path):
        value = value[p]
    return value

def set_to(new_value):
    global formatter, path, working
    if len(path) == 0:
        working = new_value
    else:
        target = lookup(True, True)
        target[path[-1]] = new_value

def parse_bool(value):
    if value.tolower() in ["true", "yes", "on", "t", "y", "1"]:
        return True
    if value.tolower() in ["false", "no", "off", "f", "n", "0"]:
        return False
    raise ValueError("Value was supposed to be true, false, yes, no, on, off, t, f, y, n, 1, or 0, but was " + str(value))

def read_from_stdin():
    input = ""
    try:
        while True:
            input += raw_input()
    except EOFError: # Read all the input
        pass
    return input

global formatter, path, working, warnings
args = parser.parse_args()
if args.help:
    print parser.format_help().replace("!!!DESC!!!", description.strip()
            ).replace("!!!END!!!", epilog.strip())
    sys.exit()
if not args.actions:
    print "Use json --help for more information."
    sys.exit()
warnings = args.warnings
# print args.actions
formatter = JSONFormatter()
path = []
jump_path_next = False
jump_path_now = False
working = None
file_to_write = None
formatter_to_write_with = None
for action_info in args.actions:
    if jump_path_next:
        jump_path_next = False
        jump_path_now = True
    elif jump_path_now:
        jump_path_now = False
        # print "Jumping up"
        path = path[:-1]
    # print "Action:", action_info, "Jump:", jump_path_next, jump_path_now, "Working:", working, "Path:", path
    action = action_info[0]
    params = action_info[1:]
    if action.endswith("-and-up"):
        action = action[:-len("-and-up")]
        jump_path_next = True
    # print action, params, path
    if action == "up":
        path = path[:-1]
    elif action == "key":
        path.append(params[0])
    elif action == "index":
        path.append(int(params[0]))
    elif action == "read":
        set_to(formatter.read(read_from_stdin()))
    elif action == "read-file":
        set_to(formatter.read_file(params[0]))
    elif action == "write":
        print formatter.write(working)
    elif action == "write-file":
        formatter.write_file(params[0], working)
    elif action == "write-current":
        print formatter.write(lookup())
    elif action == "write-current-to-file":
        formatter.write_file(params[0], lookup())
    elif action == "file":
        set_to(formatter.read_file(params[0]))
        file_to_write = params[0]
        formatter_to_write_with = formatter
    elif action == "object":
        set_to({})
    elif action == "list":
        set_to([])
    elif action == "string":
        set_to(params[0])
    elif action == "number":
        v = params[0]
        if int(v) == float(v):
            set_to(int(v))
        else:
            set_to(float(v))
    elif action == "bool":
        set_to(parse_bool(params[0]))
    elif action == "true":
        set_to(True)
    elif action == "false":
        set_to(False)
    elif action == "null":
        set_to(None)
    elif action == "value":
        set_to(formatter.read(params[0]))
    elif action == "json":
        formatter = JSONFormatter()
    elif action == "plist":
        formatter = PlistFormatter()
    elif action == "python":
        if warnings:
            print "Python mode is unsafe. You probably don't want to use it. If you really do, use -S to override this warning."
            sys.exit()
        formatter = PythonFormatter()
    elif action == "text":
        formatter = TextFormatter()
    elif action == "delete":
        v = lookup(True)
        del v[path[-1]]
    elif action == "append":
        v = lookup()
        v.append(None)
        path.append(len(v) - 1)
    elif action == "insert":
        v = lookup()
        index = int(params[0])
        v.insert(index, None)
        path.append(index)
    elif action == "print-flipped":
        v = lookup()
        print flipped(v)
    elif action == "size":
        v = lookup()
        print len(v)
    else:
        raise Exception("Unknown action (this is usually a bug in the JSON command): " + action)
if file_to_write is not None:
    formatter_to_write_with.write_file(file_to_write, working)
        




