package WWW::TheEchoNest;

use Moose;
#use Log::Log4perl qw(:easy);

#with 'MooseX::Log::Log4perl::Easy';

BEGIN {
    $WWW::TheEchoNest::VERSION = "0.1";
    # Log::Log4perl->easy_init();
}

use Data::Dumper;
use URI;
use URI::Escape;
use WWW::Mechanize;
use JSON::XS;
use JSON::Path;
use XML::Simple;
use HTTP::Headers;
use Scalar::Util;
use File::Basename;
use IO::CaptureOutput qw( capture qxx qxy );
use Digest::MD5::File qw( file_md5_hex url_md5_hex );
use WWW::TheEchoNest::Codegen;

# use File::Which (future work for auto finding ffmpeg program)
# use File::Fetch (assist with bringing remote data, http, ftp, etc)

# ABSTRACT: Wrapper for The Echo Nest API of music intelligence

has 'api_key' => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

has 'version' => (
    is       => 'rw',
    isa      => 'Str',
    default  => 'v4',
);

has 'result_format' => (
    is       => 'rw',
    isa      => 'Str',
    default  => 'json',
);

has 'results' => (
    is       => 'rw',
    isa      => 'Int',
    default  => '15'
);

has 'debug' => (
    is       => 'rw',
    isa      => 'Int',
    default  => 0,
);

has 'uri_scheme' => (
    is       => 'rw',
    isa      => 'Str',
    default  => 'http',
);

has uri_hostname => (
    is       => 'rw',
    isa      => 'Str',
    default  => 'developer.echonest.com'
);

has uri_domain_path => (
    is       => 'rw',
    isa      => 'Str',
    default  => 'api',
);

has call_type => (
    is       => 'rw',
    isa      => 'Str',
);

has auto_json_decode => (
    is       => 'rw',
    isa      => 'Int',
    default  => 0
);

has auto_xml_decode => (
    is       => 'rw',
    isa      => 'Int',
    default  => 0
);

has codegen_command => (
    is        => 'rw',
    isa       => 'Str',
    default   => q{}
);

has seed_limit => (
    is        => 'rw',
    isa       => 'Int',
    default   => '5'
);

has last_result => (
    is        => 'rw',
    isa       => 'Str',
    default   => q{}
);

has last_error => (
    is        => 'rw',
    isa       => 'Str',
    default   => q{}
);

has response_headers => (
    is        => 'rw',
    isa       => 'Str',
    default   => q{}
);

has problem => (
    is        => 'rw',
    isa       => 'Str',
    default   => q{}
);

my %code_to_name = (

'-1' =>	'Unknown Error',
0 =>	'Success',
1 =>	'Missing/ Invalid API Key',
2 =>	'This API key is not allowed to call this method',
3 =>	'Rate Limit Exceeded',
4 =>	'Missing Parameter',
5 =>	'Invalid Parameter'
);

# ADD CODE HERE TO READ A CONFIG FILE IN /etc or ~ depending on platform
# NOT HIGH PRIORITY (to original author)

# Intitial plan is to create this with type as the key
# and then a two element array as the value with
# element 1 being name and element 2 being if it is optional
# need to address the dependency model some how
# the third element is if it allows multiple values

my %call_type_attributes = ( 
  'artist/biographies' => [
        [ 'id|name' ,  1 ],
        [ 'format'  ,  0 ],
        [ 'callback' , 0 ],
        [ 'results',   0 ],
        [ 'start' ,    0 ],
        [ 'license' ,  0 , 1 ]
                            ],

   'artist/blogs' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ],
        [ 'high_relevnace' , 0 ]
    
   ],

   'artist/familiarity' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],    
   ],
   
   'artist/hotttnesss' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'type'           , 0 ],
   ],
   
   'artist/images' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ],
        [ 'license'        , 0 , 1 ],
   ],
   
   'artist/list_genres' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
   ],   
   
   'artist/list_terms' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'type'           , 0 ]   
   ],   
   
   'artist/news' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ],
        [ 'high_relevnace' , 0 ]
    
   ],

   'artist/profile' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'bucket'         , 0 , 1 ],
   ],
   
   'artist/reviews' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ],
   ],
   
   'artist/search' => [
        [ 'name'        , 0 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'bucket', 0 , 1 ], 
        [ 'limit', 0  ], 
        [ 'artist_location', 0  ], 
        [ 'description', 0  ], 
        [ 'genre', 0 , 1 ], 
        [ 'style', 0 , 1 ], 
        [ 'mood', 0 , 1 ], 
        [ 'rank_type', 0  ], 
        [ 'fuzzy_match', 0  ], 
        [ 'max_familiarity', 0  ], 
        [ 'min_familiarity', 0  ], 
        [ 'max_hotttnesss', 0  ], 
        [ 'min_hotttnesss', 0  ], 
        [ 'artist_start_year_before', 0  ], 
        [ 'artist_start_year_after', 0  ], 
        [ 'artist_end_year_before', 0  ], 
        [ 'artist_end_year_after', 0  ], 
        [ 'sort', 0  ], 
        [ 'results', 0  ], 
        [ 'start', 0  ], 
    
   ],
   
    'artist/extract' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'bucket', 0 , 1  ], 
        [ 'limit', 0  ],
        [ 'text' , 0 ],
        [ 'max_familiarity', 0  ], 
        [ 'min_familiarity', 0  ], 
        [ 'max_hotttnesss', 0  ], 
        [ 'min_hotttnesss', 0  ],     
        [ 'sort'           , 0 ],
        [ 'results' , 0 ]
    
   ],

   'artist/songs' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ]
   ],
   
   'artist/similar' => [
        [ 'id|name'        , 1 , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'min_results'        , 0 ],
        [ 'start'          , 0 ],
        [ 'bucket', 0 , 1 ], 
        [ 'limit', 0  ], 
        [ 'artist_location', 0  ], 
        [ 'name', 0  ], 
        [ 'description', 0  ], 
        [ 'genre', 0  ], 
        [ 'style', 0  ], 
        [ 'mood', 0  ], 
        [ 'rank_type', 0  ], 
        [ 'fuzzy_match', 0  ], 
        [ 'max_familiarity', 0  ], 
        [ 'min_familiarity', 0  ], 
        [ 'max_hotttnesss', 0  ], 
        [ 'min_hotttnesss', 0  ], 
        [ 'artist_start_year_before', 0  ], 
        [ 'artist_start_year_after', 0  ], 
        [ 'artist_end_year_before', 0  ], 
        [ 'artist_end_year_after', 0  ], 
        [ 'seed_catalog', 0 , 1 ]
    
   ],
   
   'artist/suggest' => [
        [ 'name'           , 0 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'q'              , 0 ]
   ],   
   
   'artist/terms' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'sort'           , 0 ]    
   ],
   
   'artist/top_hottt' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ],
        [ 'genre'          , 0 ],
        [ 'bucket'         , 0 , 1 ],
        [ 'limit'          , 0 ]
   ],

   'artist/top_terms' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ]
   ],
   
   'artist/twitter' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ]    
   ],

   'artist/urls' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ]
   
   ],

   'artist/video' => [
        [ 'id|name'        , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'results'        , 0 ],
        [ 'start'          , 0 ]
   ],
   
   # SONG PARAMETERS
   
    'song/search' => [
        [ 'format', 0  ],
        [ 'title', 0  ],
        [ 'artist', 0  ],
        [ 'combined', 0  ],
        [ 'description', 0 , 1 ],
        [ 'style', 0 , 1 ],
        [ 'mood', 0 , 1 ],
        [ 'rank_type', 0  ],
        [ 'artist_id', 0  ],
        [ 'results', 0  ],
        [ 'start', 0  ],
        [ 'song_type', 0 , 1 ],
        [ 'max_tempo', 0  ],
        [ 'min_tempo', 0  ],
        [ 'max_duration', 0  ],
        [ 'min_duration', 0  ],
        [ 'max_loudness', 0  ],
        [ 'min_loudness', 0  ],
        [ 'artist_max_familiarity', 0  ],
        [ 'artist_min_familiarity', 0  ],
        [ 'artist_start_year_before', 0  ],
        [ 'artist_start_year_after', 0  ],
        [ 'artist_end_year_before', 0  ],
        [ 'artist_end_year_after', 0  ],
        [ 'song_max_hotttnesss', 0  ],
        [ 'song_min_hotttnesss', 0  ],
        [ 'artist_max_hotttnesss', 0  ],
        [ 'artist_min_hotttnesss', 0  ],
        [ 'min_longitude', 0  ],
        [ 'max_longitude', 0  ],
        [ 'min_latitude', 0  ],
        [ 'max_latitude', 0  ],
        [ 'max_danceability', 0  ],
        [ 'min_danceability', 0  ],
        [ 'max_energy', 0  ],
        [ 'min_energy', 0  ],
        [ 'mode', 0  ],
        [ 'key', 0  ],
        [ 'bucket', 0 , 1 ],
        [ 'sort', 0  ],
        [ 'limit', 0  ]    
        
    ],

    'song/profile' => [
        [ 'id|track_id' , 1 ],
        [ 'format' , 0 ],
        [ 'bucket' , 0 , 1 ],
        [ 'limit'  , 0 ]
    ],
    
    'song/identify' => [
        [ 'query', 0  ],
        [ 'code', 0  ],
        [ 'artist', 0  ],
        [ 'title', 0  ],
        [ 'release', 0  ],
        [ 'duration', 0  ],
        [ 'genre', 0  ],
        [ 'version', 0  ],
        [ 'bucket', 0 , 1 ],
    ],
    
    # TRACK ATTRIBUTES
    
    'track/profile' => [
        [ 'id|md5' , 1 ],
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'bucket'         , 0 , 1 ]    
    ],
    
    'track/upload' => [
        [ 'format'         , 0 ],
        [ 'callback'       , 0 ],
        [ 'url|filetype'   , 1 ]
    ],
    
    # PLAYLIST PARAMETERS
    
    'playlist/basic' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'type', 0  ],
        [ 'artist_id', 0 , 1 ],
        [ 'artist', 0 , 1 ],
        [ 'song_id', 0 , 1 ],
        [ 'genre', 0  ],
        [ 'track_id', 0 , 1 ],
        [ 'results', 0  ],
        [ 'bucket', 0 , 1 ],
        [ 'limit', 0  ],
        [ 'dmca', 0  ],
        [ 'limited_interactivity' , 0    ]
    ],
    
    'playlist/static' => [
        [ 'format', 0                    ],
        [ 'callback', 0                  ],
        [ 'type', 0                      ],
        [ 'artist_pick', 0               ],
        [ 'variety', 0                   ],
        [ 'distribution', 0              ],
        [ 'adventurousness', 0           ],
        [ 'artist_id', 0  , 1            ],
        [ 'artist', 0 , 1                ],
        [ 'seed_catalog', 0              ],
        [ 'song_id', 0 , 1               ],
        [ 'track_id', 0  , 1             ],
        [ 'genre' , 0                    ],
        
        # these are considered premium as of Sept 2013
        [ 'description', 0 , 1           ],
        [ 'style', 0  , 1                ],
        [ 'mood', 0   , 1                ],
        [ 'results', 0                   ],
        [ 'max_tempo', 0                 ],
        [ 'min_tempo', 0                 ],
        [ 'max_duration', 0              ],
        [ 'min_duration', 0              ],
        [ 'max_loudness', 0              ],
        [ 'min_loudness', 0              ],
        [ 'max_danceability', 0          ],
        [ 'min_danceability', 0          ],
        [ 'max_energy', 0                ],
        [ 'min_energy', 0                ],
        [ 'artist_max_familiarity', 0    ],
        [ 'artist_min_familiarity', 0    ],
        [ 'artist_max_hotttnesss', 0     ],
        [ 'artist_min_hotttnesss', 0     ],
        [ 'artist_start_year_before', 0  ],
        [ 'artist_start_year_after', 0   ],
        [ 'artist_end_year_before', 0    ],
        [ 'artist_end_year_after', 0     ],
        [ 'song_type', 0 , 1             ],
        [ 'song_max_hotttnesss', 0       ],
        [ 'song_min_hotttnesss', 0       ],
        [ 'min_longitude', 0             ],
        [ 'max_longitude', 0             ],
        [ 'min_latitude', 0              ],
        [ 'max_latitude', 0              ],
        [ 'mode', 0 , 1                  ],
        [ 'key', 0                       ],
        [ 'bucket', 0 , 1                ],
        [ 'sort', 0                      ],
        [ 'limit', 0                     ],
        [ 'dmca', 0                      ],
        [ 'limited_interactivity' , 0    ]
    ],
   'playlist/dynamic/create' => [
        [ 'session_catalog' , 0 , 1 ],
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'type', 0  ],
        [ 'artist_pick', 0  ],
        [ 'variety', 0  ],
        [ 'distribution', 0  ],
        [ 'adventurousness', 0  ],
        [ 'artist_id', 0  ],
        [ 'artist', 0  ],
        [ 'seed_catalog', 0  ],
        [ 'song_id', 0  ],
        [ 'track_id', 0  ],
        [ 'description', 0  ],
        [ 'style', 0  ],
        [ 'mood', 0  ],
        [ 'max_tempo', 0  ],
        [ 'min_tempo', 0  ],
        [ 'max_duration', 0  ],
        [ 'min_duration', 0  ],
        [ 'max_loudness', 0  ],
        [ 'min_loudness', 0  ],
        [ 'max_danceability', 0  ],
        [ 'min_danceability', 0  ],
        [ 'max_energy', 0  ],
        [ 'min_energy', 0  ],
        [ 'artist_max_familiarity', 0  ],
        [ 'artist_min_familiarity', 0  ],
        [ 'artist_max_hotttnesss', 0  ],
        [ 'artist_min_hotttnesss', 0  ],
        [ 'artist_start_year_before', 0  ],
        [ 'artist_start_year_after', 0  ],
        [ 'artist_end_year_before', 0  ],
        [ 'artist_end_year_after', 0  ],
        [ 'song_max_hotttnesss', 0  ],
        [ 'song_min_hotttnesss', 0  ],
        [ 'min_longitude', 0  ],
        [ 'max_longitude', 0  ],
        [ 'min_latitude', 0  ],
        [ 'max_latitude', 0  ],
        [ 'mode', 0  ],
        [ 'key', 0  ],
        [ 'bucket', 0 , 1 ],
        [ 'sort', 0  ],
        [ 'limit', 0  ],
        [ 'dmca', 0  ],
        [ 'genre', 0  ],
        [ 'limited_interactivity' , 0    ]
   ],
   
   'playlist/dynamic/restart' => [
        [ 'session_catalog' , 0 ],
        [ 'session_id' , 1 ],
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'type', 0  ],
        [ 'artist_pick', 0  ],
        [ 'variety', 0  ],
        [ 'distribution', 0  ],
        [ 'adventurousness', 0  ],
        [ 'artist_id', 0  ],
        [ 'artist', 0  ],
        [ 'seed_catalog', 0  ],
        [ 'song_id', 0  ],
        [ 'track_id', 0  ],
        [ 'description', 0  ],
        [ 'style', 0  ],
        [ 'mood', 0  ],
        [ 'results', 0  ],
        [ 'max_tempo', 0  ],
        [ 'min_tempo', 0  ],
        [ 'max_duration', 0  ],
        [ 'min_duration', 0  ],
        [ 'max_loudness', 0  ],
        [ 'min_loudness', 0  ],
        [ 'max_danceability', 0  ],
        [ 'min_danceability', 0  ],
        [ 'max_energy', 0  ],
        [ 'min_energy', 0  ],
        [ 'artist_max_familiarity', 0  ],
        [ 'artist_min_familiarity', 0  ],
        [ 'artist_max_hotttnesss', 0  ],
        [ 'artist_min_hotttnesss', 0  ],
        [ 'artist_start_year_before', 0  ],
        [ 'artist_start_year_after', 0  ],
        [ 'artist_end_year_before', 0  ],
        [ 'artist_end_year_after', 0  ],
        [ 'song_max_hotttnesss', 0  ],
        [ 'song_min_hotttnesss', 0  ],
        [ 'min_longitude', 0  ],
        [ 'max_longitude', 0  ],
        [ 'min_latitude', 0  ],
        [ 'max_latitude', 0  ],
        [ 'mode', 0  ],
        [ 'key', 0  ],
        [ 'bucket', 0  , 1 ],
        [ 'sort', 0  ],
        [ 'limit', 0  ],
        [ 'dmca', 0  ],
        [ 'limited_interactivity' , 0    ],
        [ 'genre', 0  ]
   ],
   
   'playlist/dynamic/next' => [
        [ 'format' ,    0 ],
        [ 'callback' ,  0 ],
        [ 'results' ,   0 ],
        [ 'lookahead' , 0 ],
        [ 'session_id' , 0 ]
   ],
   
   'playlist/dynamic/feedback' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'ban_artist', 0  ],
        [ 'favorite_artist', 0  ],
        [ 'ban_song', 0  ],
        [ 'skip_song', 0  ],
        [ 'favorite_song', 0  ],
        [ 'play_song', 0  ],
        [ 'unplay_song', 0  ],
        [ 'rate_song', 0  ],
        [ 'update_catalog', 0  ],
        [ 'invalidate_song', 0  ],
        [ 'invalidate_artist', 0  ],
        [ 'session_id' , 0 ]
   ],
   
   'playlist/dynamic/steer' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'min_xxx', 0  ],
        [ 'max_xxx', 0  ],
        [ 'target_xxx', 0  ],
        [ 'more_like_this', 0  ],
        [ 'less_like_this', 0  ],
        [ 'adventurousness', 0  ],
        [ 'variety', 0  ],
        [ 'description', 0  ],
        [ 'style', 0  ],
        [ 'song_type', 0  ],
        [ 'mood', 0  ],
        [ 'reset', 0  ],
        [ 'session_id' , 0 ]
   ],
   
   'playlist/dynamic/info' => [
        [ 'session_id' , 0 ],
        [ 'format', 0  ],
        [ 'callback', 0  ]
   ],

   'playlist/dynamic/delete' => [
        [ 'session_id' , 0 ],
        [ 'format', 0  ],
        [ 'callback', 0  ]
   ],

   # TASTE PROFILE PARAMETERS
    
    'catalog/create' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'name', 1  ],
        [ 'type', 1  ]
    ],
    
    'catalog/update' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],
        [ 'data_type', 0  ],
        [ 'data' , 0 ]
    ],
    
    'catalog/keyvalues' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],       
    ],
    
    'catalog/play' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item', 1  ],
        [ 'plays', 0  ]       
    ],

    'catalog/skip' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item', 1  ],
        [ 'skips', 0  ]       
    ],    

    'catalog/favorite' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item', 1  ],
        [ 'favorite', 0  ]       
    ],
    
    'catalog/ban' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item', 1  ],
        [ 'ban', 0  ]       
    ],
    
    'catalog/rate' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item', 1  ],
        [ 'rating', 0  ]       
    ],

    'catalog/status' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'ticket', 1  ]       
    ],
    
    'catalog/profile' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id|name', 1  ],  
    ],     
    
    'catalog/read' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'item_id', 0  ],
        [ 'bucket', 0 , 1 ],
        [ 'results' , 0 ],
        [ 'start' , 0 ]
    ],     

    'catalog/feed' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],  
        [ 'bucket', 0 , 1 ],
        [ 'results' , 0 ],
        [ 'start' , 0 ],
        [ 'since' , 0 ],
        [ 'high_relevance' , 0 ]
    ],   

    'catalog/delete' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ]
    ],
    
    'catalog/list' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'results', 0  ],
        [ 'start' , 0 ]
    ],

    'catalog/similar' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],
        [ 'results', 0  ],
        [ 'start' , 0 ],
        [ 'keyvalue' , 0 ]
    ],

    'catalog/predict' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'id', 1  ],
        [ 'category', 0  ]
    ],
    
    # SANDBOX PARAMETERS
    
    'sandbox/list' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'sandbox', 1  ],
        [ 'results', 0  ],
        [ 'start' , 0 ],
    ],
    
    'sandbox/access' => [
        [ 'format', 0  ],
        [ 'callback', 0  ],
        [ 'sandbox', 0  ],
        [ 'id', 0  ],
        [ 'oauth_nonce', 0  ],
        [ 'oauth_timestamp', 0  ],
        [ 'oauth_version', 0  ],
        [ 'oauth_signature_method', 0  ],
        [ 'oauth_consumer_key', 0  ],
        [ 'oauth_signature', 0  ]        
    ],
);

# Taken from WWW::Echonest for initial iteration

# Typenames that appear in Echonest identifier codes
my %typenames =
    (
     AR => 'artist',
     SO => 'song',
     RE => 'release',
     TR => 'track',
     PE => 'person',
     DE => 'device',
     LI => 'listener',
     ED => 'editor',
     TW => 'tweditor',
     CA => 'catalog',
    );

my $short_type = join (q{|}, keys %typenames);
my $long_type = join (q{|}, values %typenames);
    
# These regexes need better names!
my %id_regex_for =
    (
     # foreign regex example...
     # musicbrainz:artist:a74b1b7f-71a5-4011-9441-d0b5e4122711
     foreign => qr<
\A
.+? : # musicbrainz, 7digital
(?:$long_type) :
(?:[^^]+) \^?
(?:[0-9\.]+)?
\z
>xms,

     # short regex example...
     # ARH6W4X1187B99274F
     short => qr<
\A
(?:$short_type)
(?:[0-9A-Z]{16})
\^? (?:[0-9\.]+)?
\z
>xms,

     # long regex example...
     # music://id.echonest.com/RE/ARH6W4X1187B99274F
     # I just pulled this example out of a hat.
     #
     # [bps 5.28.2011]
     long => qr<
\A
music://id\.echonest\.com/
.+?/
(?:$short_type) /
(?:$short_type)
[0-9A-Z]{16}
\^? (?:[0-9\.]+)?
\z
>xms,
    );

# end code from WWW::Echonest

sub _have_valid_api_key {
    my $self = shift;
    if (length($self->api_key) == 17) {
        return 1;
    }
    return 0;
}

sub get_header_item {
    my $self = shift;
    my $attribute = shift;
    
    if (!defined $attribute) {
        $self->attribute_error('no attribute passed into get_header_item');
        return '';
    }
    
    my @lines = split(/\n/,$self->response_headers());

    my $match = '';
    foreach my $line (@lines) {

        if ($line =~ /$attribute/i) {
            my ($front,$back) = split(/:/,$line,2);
            $back =~ s/^\s+//;
            $back =~ s/\s+$//;
            $match = $back;
        }
    }
    return $match;
}

sub rate_limit {
    my $self = shift;
    return $self->get_header_item('X-Ratelimit-Limit');
}

#

#set_request_paramter - this allow for changing/adding request variables

#

sub set_request_parameter {
    # this is a convience method that allows manipulating the
    # paramters that can be passed as well as setting them to
    # requried if you want to force it for every call
    my $self = shift;
    my $request = shift;
    my $attribute = shift || return "Z";
    
    if (ref($attribute ne 'ARRAY')) {
        return "Y";
    }
    
    if (defined $call_type_attributes{$request}) {
        # print "it already exists\n";
        ${$call_type_attributes{$request}}[0] = $attribute;
        return 1;
    } else {
        # this is a new request type
        # we can just set this as the first
        # array ref
        ${$call_type_attributes{$request}}[0] = $attribute;
        # print Dumper(\%call_type_attributes);
        return 1;
    }   
}

sub get_request_parameter {
    my $self = shift;
    my $request = shift;
    my $attribute = shift || return "Z";
    
    if (!defined $call_type_attributes{$request}) {
        $self->last_error('attribute passed to get_request_parameter does not exist');
        return;
        # return "Y";
    }
    my @local_copy = @{$call_type_attributes{$request}};
    
    foreach my $item (@local_copy) {
        #print "g r p test\n";
        #print Dumper($item);
        my $check = $item->[0];
        if ($check eq $attribute) {
            my @out;
            foreach (1..scalar(@{$item})) {
                push @out , $item->[$_];
            }
            # print "In check\n";
            return @out if wantarray;
            return \@out;
        }
    }
    $self->problem("attribute $attribute not found get_request_parameter failed");
    return;
}

sub rate_limit_used {
    my $self = shift;
    return $self->get_header_item('X-Ratelimit-Used');
}

sub rate_limit_remaining {
    my $self = shift;
    return $self->get_header_item('X-Ratelimit-Remaining');
}

sub sanity_check_id {
    my $self = shift;
    my $id_to_check = shift;
    # pyechonest does a much more specific check then we are going to do
    # we are just going to test some very basic assumptions to *hopefully*
    # avoid breaking anything on the next change to allowed ids
    
    # FIX ME
    
    my $is_valid = 1;
    
    if (length($id_to_check) < 3) {
        return 0;
    }
    
    if ($id_to_check =~ /\n/m) {
        return 0;
    }
    
    return $is_valid;
}

sub attribute_error {
    my $self = shift;
    my $message = shift || '';
    if ($self->debug) {
        print "$message\n";
    } else {
        $self->problem($message);
    }
    return 1;
}

sub get {
    
    # This seemed like a simple enough method
    # but everything I tried resulted in unacceptable
    # trade offs and explict defining of the structures
    # The new method, which I hope I remember when I
    # revisit it, was to use JSON::Path
    # It is an awesome module, but a little heavy
    # on dependencies.  However I would not have been
    # able to do this in so few lines without it
    
    # Making a generalization here
    # if you use a * you are looking for an array
    # if you don't have an * you want the first 1 (or should I say you get the first 1)

    # here are some examples
    
    #    my ($url,$attribution) = $artist->get(
    #                                           'images[*].url' ,
    #                                           'images[*].license.attribution'
    #                                            
    #                                        );
    
    #    my ($url,$attribution) = $artist->get(
    #                                           'images[0].url' ,
    #                                           'images[0].license.attribution'
    #                                            
    #                                        );
    
    #    my ($url,$attribution) = $artist->get(
    #                                           'images[-1:].url' ,
    #                                           'images[-1:].license.attribution'
    #                                            
    #                                        );
   
    my ($self,@return) = @_;
    # my @return = @_;

    my @out;
    
    my $result = decode_json $self->last_result(); 
    
    my $search_ref = $result->{response};
    
    print Dumper($result) if $self->debug();
    
    foreach my $key (@return) {
        my $type = 'value';
        if ($key =~ /\*\]/) {
            $type = 'values';
        }
        
        my $jpath = JSON::Path->new("\$.response.$key");
        
        my @t_arr = $jpath->$type($result);
        
        if ($type eq 'value') {
            push @out , $t_arr[0];
        } else {
            push @out , \@t_arr;
        }
    }
    if (wantarray) {
        return @out;    
    } else {
        return $out[0];
    }
    

}


sub seed_count_met {
    my $self = shift;
    my $seed_hashref = shift;
    
    my $count = 0;
    
    foreach my $key (keys %{$seed_hashref}) {
        next if $key eq 'mood';
        next if $key eq 'style';
        $count += $seed_hashref->{$key};    
    }
    
    return 1 if $count >= $self->seed_limit;
    return 0;
}

sub part_of_seed_limit {
    my $self = shift;
    my $item = shift;
    
    my %seed_item = (
      artist => 1,
      artist_id => 1,
      song_id => 1,
      track_id => 1,
      session_catalog => 1
    );
    
    # trying to define attributes considered "seeds" and then
    # appropriately restricting on them
    # this might have a negative effect since it hides
    # some of the API from the user and could change
    # in the future
    # may want to remove this entirely
    if (exists $seed_item{$item}) {
        return 1;
    }
    return 0;
    
}

sub send_get_request {
    # need to build the URL here
    my $self = shift;
    
    my $attributes = shift;
    
    # ensure we have a semi valid api key stashed away
    if ($self->_have_valid_api_key() == 0) {
        return "won't send requests without a valid api key";
    }
    
    if (exists $attributes->{format} && $attributes->{format} =~ /json|xml|xspf|jsonp/) {
        $self->result_format($attributes->{format});
        delete $attributes->{format};
    }
    
    # || ( exists $attributes->{format} && $attributes->{format} eq 'jsonp')
    
    if ( $self->result_format eq 'jsonp' ) {
        return "Need a valid callback with format jsonp" if !$attributes->{callback};
    }
    
    # we can only send 5 (currently) restricts (aka seeds) across ALL
    # multipe parameter types
    # artist, song_id , etc
    
    my $seed_limit = $self->seed_limit();
    
    my %seed_count = ();
    
    my $call_type = $self->call_type();
    
    my $url = $self->build_url_base($call_type);
    
    # since it is a GET we can ? it
    $url .= "?";
    
    # add the api key since it should always be sent
    $url .= "api_key=" . $self->api_key();
    
    # add the format
    
    $url .= "&format=" . $self->result_format();
    
    print "'$call_type'\n" if $self->debug();
    
    # now depending on the call_type we look for attributes that *might* be passed and are *legal*
    my @from_attributes;
    foreach my $param ( @{ $call_type_attributes{$call_type} } ) {
        
        # set the third [2] element to 0 if it doesn't exist, this indicates
        # if we need to support multiple of these times (array_ref)
        
        if (scalar(@{$param}) == 2) {
            $param->[2] = 0;
        }
        
        
        if ($param->[0] =~ /(.*)\|(.*)/) {
            # this is an either or so we need to see if
            # one of the other exists
            my $t_param;
            if ( exists $attributes->{$1} ) {
                $t_param = $1;
            } 

            if ( exists $attributes->{$2} ) {
                $t_param = $2;
            } 

            if (!$t_param) {
                $self->attribute_error("Neither of these values were passed: $1 $2 , at least one must be present for " . $self->call_type() );
            }
            
            if ($t_param eq 'id') {
                $self->sanity_check_id($attributes->{$t_param});
            }
            
            # print "OK - $t_param\n";
            
            if (ref($attributes->{$t_param}) eq 'ARRAY') {            
                foreach my $item (@{$attributes->{$t_param}}) {
                    push @from_attributes , "$t_param=" . $item; 
                }                    
            } elsif (exists $attributes->{$t_param}) {
                if ($t_param eq 'id') {
                    if (
                        # $self->sanity_check_id($attributes->{$t_param})
                        $self->is_valid_id($attributes->{$t_param})
                        ) {
                        push @from_attributes , "$t_param=" . uri_escape($attributes->{$t_param});
                    }
                } else {
                    push @from_attributes , "$t_param=" . uri_escape($attributes->{$t_param});
                }
            }
            

            
        } elsif (exists $attributes->{ $param->[0] }
                 && ref($attributes->{ $param->[0] }) eq 'ARRAY'
                 && $param->[2] == 1  ) {

            # buckets are special because they can appear twice in a url
            # we will manage this via an arrayref to keep it perly and avoid
            # people thinking they can send to keys with the same name
            
            my $has_limit = $self->part_of_seed_limit($param->[0]);
            
            my $multi_type = $param->[0];
            
            if (!$self->seed_count_met(\%seed_count)) {            
            
                foreach my $item (@{$attributes->{$multi_type}}) {
                    push @from_attributes , "$multi_type=" . $item;
                    $seed_count{$multi_type}++;
                    last if $has_limit && $self->seed_count_met(\%seed_count);
                }
                
            }
        } else {
            if ($attributes->{$param->[0]}) {
                my $has_limit = $self->part_of_seed_limit($param->[0]);
                # look for the possibilty that we feed too many
                # to a seed
                if ( $has_limit && $param->[2] == 1 && $self->seed_count_met(\%seed_count) ) {
                    print "Seed limit met, skipping $param->[0]\n" if $self->debug;
                    
                } else {
                
                    print "OK (normal) - $param->[1]\n" if $self->debug();
                    push @from_attributes , "$param->[0]=" . uri_escape($attributes->{$param->[0]});
                    if ($has_limit && $param->[2] == 1) {
                        # attempting to leverage the data structure that
                        # describes this attribute to enforce limits
                        # FIX ME ?
                        $seed_count{$param->[0]}++;
                    }
                    
                    
                }
            }
        }
    }
    
    my $param_string = join('&',@from_attributes);
    if ($param_string) {
        $url .= '&' . $param_string;
    }
    
    print "$url\n" if $self->debug;
    
    my $mech = WWW::Mechanize->new();
    $mech->get( $url );
    
    #my $hd;
    #capture { $mech->dump_headers(); } \$hd;

    #$self->response_headers($hd);
    $self->_set_response_headers($mech);
    return $self->format_results($mech->content);
    
}

sub is_valid_json {
    my ($self,$json,$caller) = @_;
    eval {
        decode_json $json;    
    };
    
    if ($@) {
        $self->last_error("invalid josn passed into $caller");
        return 0;
    } else {
        return 1;
    }
}

sub send_post_request {
    my $self = shift;
    my $uri_part = shift;
    my $extra    = shift;
 
    # ensure we have a semi valid api key stashed away
    if ($self->_have_valid_api_key() == 0) {
        my $err_mes = "won't send requests without a valid api key";
        $self->last_error($err_mes);
        return $err_mes;
    }
    
    if (exists $extra->{format} && $extra->{format} =~ /json|xml|xspf|jsonp/) {
        $self->result_format($extra->{format});
        # delete $extra{format};
    }
    
    my $result_format = $self->result_format() || '';
    
    # || ( exists $extra->{format} && $extra->{format} eq 'jsonp' )
    
    if ( $result_format eq 'jsonp' && ! exists $extra->{callback} ) {
        my $err_mes = "Need a valid callback with format jsonp";
        $self->last_error($err_mes);
        return $err_mes;
    }
    
    # if we have "data" in the form elements we need to check its formatting
    # if don't we get a 500 and WWW::Mechanize doesn't like that
    
    if (exists $extra->{data}) {
        if ($self->is_valid_json($extra->{data},"$uri_part") == 0) {
            $self->last_error("invalid json passed into $uri_part");
            return;            
        }
    }
    
    $self->call_type($uri_part);
    
    $extra->{api_key} = $self->api_key();
    
    # if we are going a track/upload we have to manage this slightly differently
    # the $extra data has been seeded with some additional information
    
    my $url = $self->build_url_base();
    
    if ($url !~ /^http/) {
        return "Unable to make uri, got this '$url' instead\n";
    }
    
    print $url , "\n" if $self->debug();
    
    my $mech;
    my $file;
    if ($self->call_type($uri_part) =~ /upload/ ) {
        $mech = WWW::Mechanize->new( );
        # $mech->add_header( $headers->[0] => $headers->[1] );
        $url .= "?api_key=" . $self->api_key();
        $url .= "&filetype=" . $extra->{filetype};
        
        # $mech->filename($extra->{url} || $extra->{track});
        # delete $extra->{filetype};
        
        my $file_contents;
        
        if ($extra->{url} =~ /^(http|ftp)/) {
            my $mech_download = WWW::Mechanize->new();
            $mech_download->get($extra->{url});
            $file_contents = $mech_download->content;
        } else {
        
            open( my $fh , "<" , "$extra->{url}" ) || die "Can't open $extra->{url}: $!";
        
            binmode $fh;
            
            while ( my $chunk = <$fh> ) {
                $file_contents .= $chunk;
            }
        }

        $mech->post( $url ,
                    'Content-Type' => 'application/octet-stream',
                    'Content' => $file_contents
                    );
        
    } else {
        $mech = WWW::Mechanize->new(  );
        $mech->post( $url , [ $extra ] );
    }
    
    
    $self->_set_response_headers($mech);
    $self->last_error('');
    return $self->format_results($mech->content);

}

sub _set_response_headers {
    my $self = shift;
    my $mech = shift;
    
    my $hd;
    capture { $mech->dump_headers(); } \$hd;

    $self->response_headers($hd);
    return;
}

sub build_url_base {
    # first the uri type
    my $self = shift;
    my $call_type = shift || $self->call_type();
    
  
    my $url = $self->uri_scheme();
    
    # the ://
    $url .= "://";
    
    # the domain
    $url .= $self->uri_hostname();
    
    # the path
    if ( $self->uri_domain_path() ) {
        $url .= "/" . $self->uri_domain_path();
    }
 
    # the version
    if ( $self->uri_domain_path() ) {
        $url .= "/" . $self->version();
    }

        # the path
    if ( $call_type ) {
        $url .= "/" . $call_type;
    }
    return $url;
}

sub request_with_minimum {
    my $self     = shift;
    my $uri_part = shift;
    my $extra    = shift;
    
    if (defined $call_type_attributes{$uri_part}) {
        if (scalar keys %{$extra} == 0) {
            $self->attribute_error("'$uri_part' requires at least 1 parameter to be passed");
        } else {
            $self->call_type($uri_part);
            return $self->send_get_request($extra);
        }
    } else {
        $self->attribute_error("invalid API call, either it is a typo or this feature doens't exist yet ($uri_part)");
    }
    return "error";
}

sub request_with_no_minimum {
    my $self     = shift;
    my $uri_part = shift;
    my $extra    = shift;
    
    if (defined $call_type_attributes{$uri_part}) {    
        $self->call_type($uri_part);
        return $self->send_get_request($extra);
    } else {
        $self->attribute_error("invalid API call, either it is a typo or this feature doesn't exist yet ($uri_part)");
    }
    return "error";
}

sub check_result_code {
    my ($self,$num_only) = @_;

    my ($code) = $self->get( 'status.code' );
    if ($num_only) {
        return $code;
    }
    
    return $code_to_name{$code};
}

sub is_valid_id {
    # FIX ME
    # need to make the id that was passed is of a valid format
    # typically just need to make with it has a sane struture
    # if and how do other libraries do this?
    
    # borrowed code from the WWW::EchoNest Perl lib
    # see comments in that data structure else where
    # in this file
    
    my ($self,$identifier) = @_;
    # my ($identifier) = @_;

    return if ! defined($identifier);
    
    for my $id_regex (values %id_regex_for) {
        return 1 if $identifier =~ /$id_regex/;
    }
    
    return;
    
    
}

sub get_file_extension {
    my ($self,$uri) = @_;
    if (!$uri) {
        return "no uri passed into get_filetype";
    }
    
    my $basename;
    
    if ($uri =~ /^(http|ftp)/) {
        my $uri_obj = URI->new($uri);
        $basename = ( $uri_obj->path_segments )[-1];
    } else {
        $basename = $uri;
    }
    
    my @extensions = qw( wav mp3 au ogg m4a mp4 );
    
    my ($dir,$name,$ext) = fileparse($basename);
    
    if ($basename && $ext) {
        return $ext;
    }
    return;
}

sub format_results {
    my $self = shift;
    my $content = shift;
    
    # want to store the result in case
    # we want to interact with it via a helper method
    $self->last_result($content);
    
    # FIX ME / TEST ME
    # vefify both of these work and return the *same* perl hash
    
    # when / how should we check the status? Do we need to?
    # if so then we need to create another method that will
    # manage a Sucess vs. Fail request
    
    if ($self->auto_json_decode && $self->result_format eq 'json' ) {
        return decode_json $content;
    }

    if ($self->auto_xml_decode && $self->result_format eq 'xml' ) {
        # FIX ME
        my $xs = XML::Simple->new();
        return $xs->XMLin($content);
    }
    
    # results are not altered in this cass and would be either
    # json or xml instead of a perl data structure
    
    return $content;
}

sub get_resource_md5 {
    
    my ($self,$resource) = @_;
    
    my $md5;
     
    if ($resource =~ /^http|ftp/) {
        # getting data from online
        # lets see if we can avoid the actaul analysis and get an md5
        $md5 = url_md5_hex($resource);   
    } else  {
        # using a local audio file
        # do we need to upload it? see if there is already an md5 available for it
        # warn "ends with approriate file extension\n";
        $md5 = file_md5_hex($resource);
    }
    
    return $md5;
}

no Moose;
__PACKAGE__->meta->make_immutable;

1;

__END__

=pod

=head1 NAME

WWW::TheEchoNest - Wrapper for The Echo Nest API of music intelligence

=head1 VERSION

version 0.1

=head1 DESCRIPTION

WWW::TheEchoNest provides a robust wrapper around The Echo Nest API to provide
access to nearly all the current features and provide some flexibility should
features change without having to rely on an update to this module

=head1 NAME

WWW::TheEchoNest

=head1 REQUIRES

L<Moose> 

L<IO::CaptureOutput> 

L<Scalar::Util> 

L<HTTP::Headers> 

L<XML::Simple> 

L<JSON::Path> 

L<JSON::XS> 

L<WWW::Mechanize> 

L<URI::Escape> 

=head1 INTERNAL METHODS

These typically never need to be used directly, but are provided here in case
you feel the need to look under the hood.

=head2 attribute_error

 attribute_error();

=head2 build_url_base

 build_url_base();

=head2 check_result_code

 check_result_code();

=head2 format_results

 format_results();

=head2 get

 get();

=head2 get_header_item

 get_header_item();

=head2 get_request_parameter

 get_request_parameter();

=head2 is_valid_id

 is_valid_id();

=head2 part_of_seed_limit

 part_of_seed_limit();

=head2 rate_limit

 rate_limit();

=head2 rate_limit_remaining

 rate_limit_remaining();

=head2 rate_limit_used

 rate_limit_used();

=head2 request_with_minimum

 request_with_minimum();

=head2 request_with_no_minimum

 request_with_no_minimum();

=head2 sanity_check_id

 sanity_check_id();

=head2 seed_count_met

 seed_count_met();

=head2 send_get_request

 send_get_request();

=head2 send_post_request

 send_post_request();

=head2 set_request_parameter

 set_request_parameter();

=head1 AUTHOR

Aaron Johnson <aaronjjohnson@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by Aaron Johnson.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
