# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package LJ::JSON;
use strict;

use base qw(Exporter);
@LJ::JSON::EXPORT = qw( from_json to_json );

=head1 NAME

LJ::JSON - Wrapper for JSON which handles text encoding

=head1 SYNOPSIS

  use LJ::JSON; # never use JSON! That could introduce subtle encoding issues

  my $is_yummy = 1;
  my $json_string = to_json( { a => "apple",
                        yummy  => LJ::JSON->to_boolean( $is_yummy ) # true/false
                        yummy2 => $is_yummy                         # 1/0
                                                                    # both can work in JS, but prefer true/false
                    } );
  my $object      = from_json( q!{ "a": "apple" }! );

=cut

my $wrap;

sub to_json ($@) {
    my (@args) = @_;
    return $wrap->encode(@args);
}

sub from_json ($@) {
    my ($dump) = @_;

    return unless $dump;
    return $wrap->decode($dump);
}

sub class {
    return ref $wrap;
}

sub true  { $wrap->true }
sub false { $wrap->false }

sub to_boolean {
    my ( $class, $what ) = @_;
    return $what ? $wrap->true : $wrap->false;
}

sub to_number {
    my ( $class, $what ) = @_;

    # not using int deliberately because we may be handling floats here
    return $what + 0;
}

foreach my $class (qw(LJ::JSON::XS LJ::JSON::JSONv2 LJ::JSON::JSONv1)) {
    if ( $class->can_load ) {
        $wrap = $class->new;
        last;
    }
}
die unless $wrap;

1;

package LJ::JSON::Wrapper;

use Encode qw();

sub traverse {
    my ( $class, $what, $sub ) = @_;

    my $type = ref $what;

    # simple scalar
    if ( $type eq '' ) {
        return $sub->($what);
    }

    # hashref
    if ( $type eq 'HASH' ) {
        my %ret;
        foreach my $k ( keys %$what ) {
            $ret{ $sub->($k) } = $class->traverse( $what->{$k}, $sub );
        }
        return \%ret;
    }

    # arrayref
    if ( $type eq 'ARRAY' ) {
        my @ret;
        foreach my $v (@$what) {
            push @ret, $class->traverse( $v, $sub );
        }
        return \@ret;
    }

    # unknown type; let the subclass decode it to a scalar
    # (base class function defaults to plain stringification)
    return $sub->( $class->decode_unknown_type($what) );
}

sub traverse_fix_encoding {
    my ( $class, $what ) = @_;

    return $class->traverse(
        $what,
        sub {
            my ($scalar) = @_;

            return $scalar unless Encode::is_utf8($scalar);

            # if the string does indeed contain wide characters (which happens
            # in case the source string literals contained chars specified as
            # '\u041c'), encode stuff as utf8
            if ( $scalar =~ /[^\x01-\xff]/ ) {
                return Encode::encode( "utf8", $scalar );
            }

            return Encode::encode( "iso-8859-1", $scalar );
        }
    );
}

sub decode_unknown_type {
    my ( $class, $what ) = @_;

    return "$what";
}

package LJ::JSON::XS;

our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON::XS); }

sub can_load {
    eval { require JSON::XS; JSON::XS->import; };
    return !$@;
}

sub new {
    my ($class) = @_;
    return $class->SUPER::new->latin1;
}

sub decode {
    my ( $class, $dump ) = @_;

    my $decoded = $class->SUPER::decode($dump);
    $decoded = $class->traverse_fix_encoding($decoded);
    return $decoded;
}

sub decode_unknown_type {
    my ( $class, $what ) = @_;

    # booleans get converted to undef for false and 1 for true
    return $what ? 1 : 0 if JSON::XS::is_bool($what);

    # otherwise, stringify
    return "$what";
}

1;

package LJ::JSON::JSONv2;

our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON); }

sub can_load {
    eval { require JSON };
    return !$@ && $JSON::VERSION ge 2;
}

sub new {
    my ($class) = @_;
    return $class->SUPER::new->latin1;
}

sub decode {
    my ( $class, $dump ) = @_;

    my $decoded = $class->SUPER::decode($dump);
    $decoded = $class->traverse_fix_encoding($decoded);
    return $decoded;
}

sub decode_unknown_type {
    my ( $class, $what ) = @_;

    # booleans get converted to 0 for false and 1 for true
    return $what ? 1 : 0 if JSON::is_bool($what);

    # otherwise, stringify
    return "$what";
}

1;

package LJ::JSON::JSONv1;

our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON); }

sub can_load {
    eval { require JSON };
    return !$@ && $JSON::VERSION ge 1;
}

*encode = \&JSON::objToJson;
*decode = \&JSON::jsonToObj;

1;
