import pickle
import re
import nltk
from nltk.tokenize import TweetTokenizer
import string
from textblob import TextBlob
import json

class twitterAnalyzer:
    """
    Generic class with text analysis functions specifically designed for tweets
    """

    def __init__(self, tweets, tweets_data=None, sentiment_data=None, positive_tweets=None, negative_tweets=None, neutral_tweets=None):
        """
        Class Constructur/Initializaiton Method
        """
        if tweets_data is None:
            tweets_data = []
        if sentiment_data is None:
            sentiment_data = []
        if positive_tweets is None:
            positive_tweets = []
        if negative_tweets is None:
            negative_tweets = []
        if neutral_tweets is None:
            neutral_tweets = []

        self.tweets = tweets
        self.tweets_data = tweets_data
        self.sentiment_data = sentiment_data
        self.positive_tweets = positive_tweets
        self.negative_tweets = negative_tweets
        self.neutral_tweets = neutral_tweets

    def tokenize(self):
        """
        Uses the built in tokenize function in the nltk module for tweets.
        """
        tknzr = TweetTokenizer()
        tkn = []
        for tweet in self.tweets:
            for word in tknzr.tokenize(tweet):
                tkn.append(word)
        return tkn

    def clean_tweet(self, tweet):
        """
        This function, unlike the tokenize function from the nltk package, clean the tweet text by removing all links and special characters using regex statements.
        """
        return ' '.join(re.sub("(@[A-Za-z0-9]+)|([^0-9A-Za-z \t])|(\w+:\/\/\S+)", " ", tweet).split())

    def clean_all_tweets(self):
        """
        Cleans the list of all tweets using the regex utlity function.

        Returns a list of cleaned tweet.
        """
        clean_tweets = []
        for tweet in self.tweets:
            clean_tweets.extend(self.clean_tweet(tweet).split())
        return clean_tweets

    def to_lower(self, word_list):
        """
        Makes all text lowercase.

        Returns a list of words that are all lowercase.
        """
        return [word.lower() for word in word_list]

    def word_freq(self, word_list):
        """
        Returns a histogram of all words and their frequency from a list of words.
        """
        hist = {}
        for word in word_list:
            hist[word] = hist.get(word, 0) + 1
        return hist

    def print_most_common(self, hist, n=10):
        """
        Prints a list of the n most common words from a histogram.
        """
        t = []
        for word,freq in hist.items():
            t.append((freq, word))
        t.sort(reverse=True)

        for word,freq in t[:n]:
            print(word, '\t', freq)

    def filter_stop_words(self, word_list):
        """
        Removes all stopwords and punctuations.

        Returns a list of words without specified stopwords and punctuations.
        """
        punctuation = list(string.punctuation)
        file = open("stopwords.txt")
        stopwords = []
        strippables = string.punctuation + string.whitespace
        for line in file:
            stopwords.append(line.strip(strippables))
        stopwords.extend(punctuation)

        terms_without_stop = [word for word in word_list if word not in stopwords]

        return terms_without_stop

    def tweet_sentiment_analysis(self, tweet):
        """
        Return whether a tweet is positive, negative, or neutral.

        Uses the textblob module.
        TextBlob.sentiment gives a polarity and subjectivity score. Subjectivity is from 0 to 1 and determines are factual vs opinionated the statement is.

        Returns a string.
        """
        analysis = TextBlob(self.clean_tweet(tweet))

        if analysis.sentiment.polarity > 0:
            return ['Positive', analysis.sentiment.polarity, analysis.sentiment.subjectivity]
        elif analysis.sentiment.polarity == 0:
            return ['Neutral', analysis.sentiment.polarity, analysis.sentiment.subjectivity]
        else:
            return ['Negative', analysis.sentiment.polarity, analysis.sentiment.subjectivity]

    def do_sentiment_analysis(self):
        """
        Does sentiment analysis on all tweets

        Sets the following attributes:
        self.sentiment_data
        self.positive_tweets
        self.negative_tweets
        self.neutral_tweets

        Return a list of objects with the following form:
        {
            'text': String that is the tweet
            'sentiment': String that is 'Positive', 'Negative' or 'Neutral'
            'polarity': Float from -1 to 1
            'subjectivity': Float from 0 to 1
        }
        """

        tweets_sentiment = []

        for tweet in self.tweets:
            parsed_tweet = {}
            parsed_tweet['text'] = tweet
            sentiment_data = self.tweet_sentiment_analysis(tweet)
            parsed_tweet['sentiment'] = sentiment_data[0]
            parsed_tweet['polarity'] = sentiment_data[1]
            parsed_tweet['subjectivity'] = sentiment_data[2]

            tweets_sentiment.append(parsed_tweet)

        self.sentiment_data = tweets_sentiment
        self.positive_tweets = [tweet for tweet in self.sentiment_data if tweet['sentiment'] == 'Positive']
        self.negative_tweets = [tweet for tweet in self.sentiment_data if tweet['sentiment'] == 'Negative']
        self.neutral_tweets = [tweet for tweet in self.sentiment_data if tweet['sentiment'] == 'Neutral']

        return tweets_sentiment

    def print_recent_tweets(self, sentiment, count=5):
        """
        Print specifieid number of most recent tweets that are positive.

        Sentiment is a string value that must be the following:
        'positive'
        'negative'
        'neutral'

        Count is an intenger that represents the number of tweets to print; the default is 5.
        """
        print("\nMost recent {} tweets:".format(sentiment))

        def print_tweet(tweets, count):
            for tweet in tweets[:count]:
                print(tweet['text'], "\n")

        if sentiment == 'positive':
            print_tweet(self.positive_tweets, count)
        elif sentiment == 'negative':
            print_tweet(self.negative_tweets, count)
        elif sentiment == 'neutral':
            print_tweet(self.neutral_tweets, count)
        else:
            raise ValueError("Sentiment must be a string that is 'positive', 'negative', or 'neutral'.")

    def print_extreme_tweets(self, sentiment, count=1, num_score=False):
        """
        Prints the tweet with the largest positive or negative polarity value.

        Count is an integer that represetnts the number of tweets to print after the data is sourted; the deafult is 1.

        num_score determines whether or not to print the polarity and subjectivity score of the tweet as well.
        """
        def return_polarity(tweet):
            return tweet['polarity']

        print("The top {} most {} tweets:".format(count, sentiment))

        if sentiment == 'positive':
            sorted_tweet = sorted(self.positive_tweets, key=return_polarity, reverse=True)
        elif sentiment == 'negative':
            sorted_tweet = sorted(self.negative_tweets, key=return_polarity)
        else:
            raise ValueError("Sentiment must be a string that is either 'positive' or 'negative'.")

        for tweet in sorted_tweet[:count]:
            print(tweet['text'])
            if num_score:
                print("Polarity: {} | Subjectivity: {}".format(tweet['polarity'], tweet['subjectivity']), "\n")

    def print_objective_tweets(self, count=5, objective=True):
        """
        Print tweets with the highest or lowest level of objectivity.

        Count is an integer that determines how many tweets to print.

        Objective is a boolean value that determines whether to print highly subjective or highly objective tweets.
        """
        def return_subjectivity(tweet):
            return tweet['subjectivity']

        print("Most objective tweets:")
        if objective:
            sorted_objective = sorted(self.sentiment_data, key=return_subjectivity)
        elif not objective:
            sorted_objective = sorted(self.sentiment_data, key=return_subjectivity, reverse=True)
        else:
            raise ValueError('Objective must be a boolean value')

        for tweet in sorted_objective[:count]:
            print(tweet['text'])
            print("Subjectivity: {}".format(tweet['subjectivity']), "\n")

    def print_sentiment_summary(self, sentiment_data):
        """
        Print various summary statistics and example tweets based on the sentiment analysis
        """

        self.print_recent_tweets('positive')
        self.print_recent_tweets('negative')
        self.print_recent_tweets('neutral')

        self.print_extreme_tweets('positive', num_score=True)
        self.print_extreme_tweets('negative', num_score=True)

        self.print_objective_tweets(count=5)
        self.print_objective_tweets(count=5, objective=False)

def main():
    with open('trump_tweets_text.pickle', 'rb') as input_file:
        tweets = pickle.load(input_file)

    with open('trump_tweets_data.pickle', 'rb') as input_file:
        tweets_data = pickle.load(input_file)

    trump = twitterAnalyzer(tweets=tweets, tweets_data=tweets_data)
    print(len(trump.tweets))

    print(json.dumps(trump.tweets_data[0]._json, indent=4))

    # tokenize = trump.tokenize()
    # tokenize_lower = trump.to_lower(tokenize)
    # hist_tokenize = trump.word_freq(tokenize_lower)
    # trump.print_most_common(hist_tokenize, n=10)

    # # Let's compare nltk's tokenize designed for tweets to just stripping all words down bare:
    # clean_tweets = trump.clean_all_tweets()
    # clean_tweets = trump.to_lower(clean_tweets)
    # hist_clean_tweets = trump.word_freq(clean_tweets)
    # trump.print_most_common(hist_clean_tweets, n=10)

    # # What if we do both without stop-word?
    # print("Printing most common non-stop words with tokenize method:")
    # tokenize_lower_no_stop = trump.filter_stop_words(tokenize_lower)
    # hist_tokenize_no_stop = trump.word_freq(tokenize_lower_no_stop)
    # trump.print_most_common(hist_tokenize_no_stop, n=10)

    # print("Printing most common non-stop words with regex clean method:")
    # clean_tweets_no_stop = trump.filter_stop_words(clean_tweets)
    # hist_clean_tweets_no_stop = trump.word_freq(clean_tweets_no_stop)
    # trump.print_most_common(hist_clean_tweets_no_stop, n=10)

    # Do sentiment analysis
    # sentiment_tweets = trump.do_sentiment_analysis()
    # trump.print_sentiment_summary(sentiment_tweets)

if __name__ == '__main__':
    main()


import numpy as np
import datetime as dt
from geovectorslib import geod

from scipy.stats import binned_statistic
from routeparams import RouteParams
from RoutingAlg import RoutingAlg
from polars import Boat
import utils

class RoutingAlgTimeFuelMin(RoutingAlg):

    def __init__(self, route : RouteParams, time):
        RoutingAlg.__init__(self, route.start, route.finish, time)
        self.lats_per_step = route.lats_per_step
        self.lons_per_step = route.lons_per_step
        self.dist_per_step = route.dists_per_step
        self.full_dist_traveled = route.full_dist_traveled

        self.azimuth_per_step = route.azimuths_per_step[:, np.newaxis]
        self.current_variant = route.rpm

        self.lats_per_step = self.lats_per_step[:, np.newaxis]
        self.lons_per_step = self.lons_per_step[:, np.newaxis]
        self.dist_per_step = self.dist_per_step[:, np.newaxis]
        self.current_azimuth = np.array([self.azimuth_per_step[1]])

    def pruning(self,  x, y, trim=True):
        """
              generate view of the iso that only contains the longests route per azimuth segment

              Binned statistic.
              +            iso2 = prune_isochrone(iso2, 'azi02', 's02', bins, True)
              print('iso2 ',iso2)  #

                    Parameters:
                    iso: isochrone dictionary
                    x: values to binarize
                    y: values to apply max to
                    bins: bins edges, dimension is n_bins + 1
                    trim: whether return just one of max values
                    Returns:
                        pruned isochrone dictionary with max values in each bin
                   """

        mean_dist = np.mean(self.full_dist_traveled)
        gcr_point = geod.direct(
            [self.start[0]],
            [self.start[1]],
            self.gcr_azi, mean_dist)

        new_azi = geod.inverse(
            gcr_point['lat2'],
            gcr_point['lon2'],
            [self.finish[0]],
            [self.finish[1]]
        )

        azi0s = np.repeat(
            new_azi['azi1'],
            self.prune_segments + 1)

        # determine bins
        delta_hdgs = np.linspace(
            -self.prune_sector_deg_half,
            +self.prune_sector_deg_half,
            self.prune_segments + 1)  # -90,+90,181

        bins = azi0s - delta_hdgs
        bins = np.sort(bins)

        idxs = []
        bin_stat, bin_edges, bin_number = binned_statistic(
            self.last_azimuth, self.full_dist_traveled, statistic=np.nanmax, bins=bins)

        if trim:
            for i in range(len(bin_edges) - 1):
                try:
                    idxs.append(
                        np.where(self.full_dist_traveled == bin_stat[i])[0][0])
                except IndexError:
                    pass
            idxs = list(set(idxs))
        else:
            for i in range(len(bin_edges) - 1):
                idxs.append(np.where(self.full_dist_traveled == bin_stat[i])[0])
            idxs = list(set([item for subl in idxs for item in subl]))

        # Return a trimmed isochrone
        lats_new = self.lats_per_step[:, idxs]
        lons_new = self.lons_per_step[:, idxs]
        var_new = self.variants[:, idxs]
        dist_new = self.dist_per_step[:, idxs]
        curr_azi_new = self.last_azimuth[idxs]
        full_dist_new = self.full_dist_traveled[idxs]

        self.lats_per_step = lats_new
        self.lons_per_step = lons_new
        self.variants = var_new
        self.dist_per_step = dist_new
        self.last_azimuth = curr_azi_new
        self.full_dist_traveled = full_dist_new

        #print('last_azimuth', self.last_azimuth)
        #print('inx', idxs)

        # print("rpm = ",boat.get_rpm())
        # print("Used fuel", boat.get_fuel_per_time(delta_time))

    def define_initial_variants(self):
        self.define_variants()
        self.full_time_traveled = np.repeat(0., self.variant_segments + 1, axis=0)
        self.full_dist_traveled = np.repeat(self.full_dist_traveled, self.variant_segments + 1, axis=0)
        self.time = np.repeat(self.time, self.variant_segments + 1, axis=0)

    def get_current_azimuth(self):
        return self.azimuth_per_step[self.count+1]

    def update_position(self):
        self.current_lats = self.lats_per_step[self.count+1,:]
        self.current_lons = self.lons_per_step[self.count+1,:]

    def update_time(self, delta_time, bs):
        debug = True

        gcrs = geod.inverse(self.lats_per_step[self.count,:], self.lons_per_step[self.count,:], self.lats_per_step[self.count+1,:], self.lons_per_step[self.count+1,:])
        dist = gcrs['s12']
        #self.time += dt.timedelta(seconds=dist/bs)

        delta_time_calc=dist/bs
        delta_time_calc=np.round(delta_time_calc/100)*100
        if not (delta_time == delta_time_calc[0]):
            raise ValueError('delta_time=' + str(delta_time) + ' delta_time_calc=' + str(delta_time_calc))

        for iTime in range(0,self.variant_segments+1):
                self.time[iTime]+=dt.timedelta(seconds=delta_time_calc[0])
                self.full_time_traveled[iTime]+=delta_time_calc[0]

        if(debug):
            print('dist', dist)
            print('bs', bs)
            print('time = ',self.time)
            print('delta_time_calc = ', delta_time_calc)

    def update_dist(self, delta_time, bs, current_lats, current_lons):
        #return {'lat2' : self.lats_per_step[self.count,:], 'lon2' : self.lons_per_step[self.count,:]}
        pass

    def get_wind_functions(self,wt):
        debug = False
        twa = np.zeros(self.variant_segments+1)
        tws = np.zeros(self.variant_segments+1)

        for i in range(0,self.variant_segments+1):
            winds = wt.get_wind_function((self.current_lats, self.current_lons), self.time[i])
            twa[i] = winds['twa'][0]
            tws[i] = winds['tws'][0]

        if(debug):
            print('obtaining wind function for current position', self.current_lats, self.current_lons)
            print('time:', self.time[0])
            print('wind', winds)

        winds =  {'twa' : twa, 'tws' : tws}
        return winds

    def get_final_index(self):
        return 0    #dummy

    def terminate(self, boat : Boat):
        route = RoutingAlg.terminate(self, boat)
        return route


import math
import numpy as np

def dist(a, b):
    return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

# Constants
per_sqrY = 0.0017943
per_sqrX = 0.0008982
originY = 59.806291
originX = 30.099237

Umap =[] #np.zeros(321, 289)  #([[ [] ]])

for i in range(321):  # x
    temp = []
    for k in range(289):  # y
        temp.append({})
    Umap.append(temp)

def add_to_map(user_origin, user_id, prS):
    x = int((user_origin[0] - originX) // per_sqrX)
    y = int((user_origin[1] - originY) // per_sqrY)
    Umap[prS[0]][prS[1]].pop(user_id)
    Umap[x][y][user_id] = [user_origin, user_id]

def find_surround(user_origin, rad=100):
    users_around = []
    x = int((user_origin[0] - originX) // per_sqrX)
    y = int((user_origin[1] - originY) // per_sqrY)
    for i in range(-1, 2):
        for k in range(-1, 2):
            for j in Umap[x + i][y + k]:
                if dist(user_origin, j[0]) <= rad:
                    users_around.append(j)


# Note: All string methods returns new values. They do not change the original string.
# Converts the first character to upper case
# Syntax :- string.capitalize()

# Upper case the first letter in this sentence:
txt = "hello, and welcome to my world."

x = txt.capitalize()

print (x)

# See what happens if the first character is a number:
txt = "21 is my age."

x = txt.capitalize()

print (x)


# emacs: -*- mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# -*- coding: utf-8 -*-
# ex: set sts=4 ts=4 sw=4 noet:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See COPYING file distributed along with the datalad package for the
#   copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Some additional tests for search command (some are within test_base)"""

from mock import patch
from datalad import cfg
from datalad.api import Dataset, install
from nose.tools import assert_equal, assert_raises
from datalad.utils import chpwd
from datalad.tests.utils import assert_in
from datalad.tests.utils import assert_is_generator
from datalad.tests.utils import with_tempfile
from datalad.tests.utils import with_testsui
from datalad.tests.utils import SkipTest
from datalad.support.exceptions import NoDatasetArgumentFound

from datalad.api import search
from datalad.metadata import search as search_mod

from datalad.tests.utils import skip_if_no_network

@with_testsui(interactive=False)
@with_tempfile(mkdir=True)
def test_search_outside1_noninteractive_ui(tdir):
    # we should raise an informative exception
    with chpwd(tdir):
        with assert_raises(NoDatasetArgumentFound) as cme:
            list(search("bu"))
        assert_in('run interactively', str(cme.exception))

@with_tempfile(mkdir=True)
@with_tempfile(mkdir=True)
def test_search_outside1(tdir, newhome):
    with chpwd(tdir):
        # should fail since directory exists, but not a dataset
        # should not even waste our response ;)
        always_render = cfg.obtain('datalad.api.alwaysrender')
        with patch.object(search_mod, 'LOCAL_CENTRAL_PATH', newhome):
            if always_render:
                # we do try to render results which actually causes exception
                # to come right away
                assert_raises(NoDatasetArgumentFound, search, "bu")
            else:
                gen = search("bu")
                assert_is_generator(gen)
                assert_raises(NoDatasetArgumentFound, next, gen)

        # and if we point to some non-existing dataset -- the same in both cases
        # but might come before even next if always_render
        with assert_raises(ValueError):
            next(search("bu", dataset=newhome))

@with_testsui(responses='yes')
@with_tempfile(mkdir=True)
@with_tempfile()
def test_search_outside1_install_central_ds(tdir, central_dspath):
    with chpwd(tdir):
        # let's mock out even actual install/search calls
        with \
            patch.object(search_mod, 'LOCAL_CENTRAL_PATH', central_dspath), \
            patch('datalad.api.install',
                  return_value=Dataset(central_dspath)) as mock_install, \
            patch('datalad.distribution.dataset.Dataset.search',
                  new_callable=_mock_search):
            _check_mocked_install(central_dspath, mock_install)

            # now on subsequent run, we want to mock as if dataset already exists
            # at central location and then do search again
            from datalad.ui import ui
            ui.add_responses('yes')
            mock_install.reset_mock()
            with patch(
                    'datalad.distribution.dataset.Dataset.is_installed',
                    True):
                _check_mocked_install(central_dspath, mock_install)

            # and what if we say "no" to install?
            ui.add_responses('no')
            mock_install.reset_mock()
            with assert_raises(NoDatasetArgumentFound):
                list(search(".", regex=True))

            # and if path exists and is a valid dataset and we say "no"
            Dataset(central_dspath).create()
            ui.add_responses('no')
            mock_install.reset_mock()
            with assert_raises(NoDatasetArgumentFound):
                list(search(".", regex=True))

_mocked_search_results = [
    ('ds1', {'f': 'v'}),
    ('d2/ds2', {'f1': 'v1'})
]

class _mock_search(object):
    def __call__(*args, **kwargs):
        for loc, report in _mocked_search_results:
            yield loc, report

def _check_mocked_install(central_dspath, mock_install):
    gen = search(".", regex=True)
    assert_is_generator(gen)
    # we no longer do any custom path tune up from the one returned by search
    # so should match what search returns
    assert_equal(
        list(gen), [(loc, report)
                    for loc, report in _mocked_search_results])
    mock_install.assert_called_once_with(central_dspath, source='///')

@skip_if_no_network
@with_tempfile
def test_our_metadataset_search(tdir):
    # smoke test for basic search operations on our super-megadataset
    # expensive operation but ok
    ds = install(path=tdir, source="///")
    assert list(ds.search('.', report='*', regex=True))
    assert list(ds.search('.', report='*'))
    assert list(ds.search('.', report_matched=True))

    # and either we could provide output in different formats
    import simplejson
    from datalad.utils import swallow_outputs
    from datalad.api import search_
    with swallow_outputs() as cmo:
        assert list(search_('.', report='*', regex=True, format='json', dataset=ds))
        out = cmo.out
    # since this one is just absorbs all first, we can't go one by one
    assert simplejson.loads(out)

    try:
        import yaml
    except ImportError:
        raise SkipTest("no yaml module")
    with swallow_outputs() as cmo:
        assert list(search_('.', report='*', regex=True, format='yaml', dataset=ds))
        out = cmo.out
    assert yaml.load(out)

@with_tempfile
def test_search_non_dataset(tdir):
    from datalad.support.gitrepo import GitRepo
    GitRepo(tdir, create=True)
    with assert_raises(NoDatasetArgumentFound) as cme:
        list(search('smth', dataset=tdir))
    # Should instruct user how that repo could become a datalad dataset
    assert_in("datalad create --force", str(cme.exception))


# -*- coding: utf-8 -*-

from south.db import db
from django.db import models

class Migration:
    depends_on=(
        ("main","0001_initial"),
    )
    def forwards(self):
        # Adding model 'TimeSeries'
        db.create_table('pm_timeseries', (
            ('id', models.AutoField(primary_key=True)),
            ('name', models.CharField("Name", unique=True, max_length=128)),
            ('is_enabled', models.BooleanField("Is Enabled?", default=True)),
        ))
        db.send_create_signal('pm', ['TimeSeries'])
        TimeSeries = db.mock_model(model_name='TimeSeries', db_table='pm_timeseries', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField)
        # Adding model 'TimeSeriesData'
        db.create_table('pm_timeseriesdata', (
            ('id', models.AutoField(primary_key=True)),
            ('time_series', models.ForeignKey(TimeSeries, verbose_name="Time Series")),
            ('timestamp', models.IntegerField("Timestamp")),
            ('value', models.FloatField("Value", null=True, blank=True)),
        ))
        db.create_index('pm_timeseriesdata', ['timestamp'], unique=False, db_tablespace='')
        db.send_create_signal('pm', ['TimeSeriesData'])
        #
        db.create_table('pm_chart', (
            ('id', models.AutoField(primary_key=True)),
            ('name', models.CharField("Name", unique=True, max_length=128)),
        ))
        db.send_create_signal('pm', ['Chart'])
        Chart = db.mock_model(model_name='Chart', db_table='pm_chart', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField)
        #
        db.create_table('pm_chart_time_series', (
            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
            ('chart', models.ForeignKey(Chart, null=False)),
            ('timeseries', models.ForeignKey(TimeSeries, null=False))
        ))
        #
        db.execute(SP_CREATE)

    def backwards(self):
        db.execute(SP_DROP)
        # Deleting ManyToMany field
        db.delete_table("pm_chart_time_series")
        # Deleting model 'Chart'
        db.delete_table("pm_chart")
        # Deleting model 'TimeSeriesData'
        db.delete_table('pm_timeseriesdata')
        # Deleting model 'TimeSeries'
        db.delete_table('pm_timeseries')

SP_CREATE="""
CREATE OR REPLACE
FUNCTION pm_timeseries_register(CHAR,INTEGER,DOUBLE PRECISION)
RETURNS VOID
AS
$$
DECLARE
    p_ts_name   ALIAS FOR $1;
    p_timestamp ALIAS FOR $2;
    p_value     ALIAS FOR $3;
    ts_id       INTEGER;
BEGIN
    LOOP
        SELECT id
        INTO ts_id
        FROM pm_timeseries
        WHERE name=p_ts_name;

        IF FOUND THEN
            EXIT;
        ELSE
            INSERT INTO pm_timeseries(name)
            VALUES(p_ts_name);
        END IF;
    END LOOP;

    INSERT INTO pm_timeseriesdata(time_series_id,timestamp,value)
    VALUES(ts_id,p_timestamp,p_value);
END;
$$ LANGUAGE plpgsql;
"""

SP_DROP="DROP FUNCTION pm_timeseries_register(CHAR,INTEGER,DOUBLE PRECISION)"


bl_info = {
    "name": "Ref Editor",
    "author": "Christophe Seux",
    "version": (1, 0),
    "blender": (2, 78, 0),
    "location": "",
    "description": "",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Learnbgame",
}

import bpy
from bpy.app.handlers import persistent

Dupligroups={}

# Scan list of objects and return objects with linked dupligroup
def filter_dupliGroups(objects):
    dupliGroups =[]
    for o in objects :
        if o.dupli_group and o.dupli_group.library :
            dupliGroups.append(o)
    return dupliGroups

# Convert adress '/ob/ob'in bpy.data.objects['ob'].dupli_group.object['ob']
def pathConvert(path):
    blenderPath = 'bpy.data.objects'

    for index,name in enumerate(path.split('/')[1:]):
        if index ==0 :
            blenderPath+='["%s"]'%name
        else :
            blenderPath+='.dupli_group.objects["%s"]'%name

    return(eval(blenderPath))

# analyse scene for store level of linked dupli_grou in a dict
def link_analyser(objects,Path) :
    RefEditor = bpy.context.scene.RefEditor
    if not RefEditor.get('objects') :
        RefEditor['objects'] = {}

    for o in filter_dupliGroups(objects):
        #path = '%s["%s"]'%(Path,o.name)

        if not RefEditor['objects'].get(Path+'/'+o.name) :
            RefEditor['objects'][Path+'/'+o.name] = {'hide':False , 'expand' : False}

        else :
            o.hide = RefEditor['objects'][Path+'/'+o.name]['hide']

        filter_dupli = filter_dupliGroups(o.dupli_group.objects)
        if filter_dupli :
            link_analyser(filter_dupli,Path+'/'+o.name)

#bpy.context.scene.RefEditor['objects'] =Dupligroups

class refEditorSettings(bpy.types.PropertyGroup):
    search = bpy.props.StringProperty(options={'TEXTEDIT_UPDATE'})
    filterSelect = bpy.props.BoolProperty()
    objects = {}

class RefEditorHide(bpy.types.Operator):
    bl_idname = "refedit.hide"
    bl_label = "Hide linked dupligroup"

    object = bpy.props.StringProperty()

    def execute(self, context):
        object = self.object
        RefEditor = context.scene.RefEditor

        if RefEditor['objects'][object]['hide'] == True :
            pathConvert(object).hide = False
            RefEditor['objects'][object]['hide'] = False

        else :
            pathConvert(object).hide = True
            RefEditor['objects'][object]['hide'] = True

        return {'FINISHED'}

class RefEditorExpand(bpy.types.Operator):
    bl_idname = "refedit.expand"
    bl_label = "Expand dupligroup"

    object = bpy.props.StringProperty()

    def execute(self, context):
        object = self.object
        RefEditor = context.scene.RefEditor

        if RefEditor['objects'][object]['expand'] == True :
            RefEditor['objects'][object]['expand'] = False

        else :
            RefEditor['objects'][object]['expand'] = True

        return {'FINISHED'}

class RefEditorCreateProxy(bpy.types.Operator):
    bl_idname = "refedit.create_proxy"
    bl_label = "Create Proxy"

    object = bpy.props.StringProperty()

    def execute(self, context):
        object = self.object
        RefEditor = context.scene.RefEditor

        pathConvert(object)

        return {'FINISHED'}

class RefEditorPanel(bpy.types.Panel) :
    bl_label = "Reference Editor"
    bl_category = "Refs Editor"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'

    def draw_header(self, context):
        view = context.space_data
        layout = self.layout
        layout.label(icon= "OOPS")
        row = layout.row()

    def draw(self,context):
        layout = self.layout
        row = layout.row(align= True)
        row.prop(context.scene.RefEditor,'filterSelect',icon = 'RESTRICT_SELECT_OFF',text='',emboss=True)
        row.prop(context.scene.RefEditor,'search',icon = 'VIEWZOOM',text='')
        row.operator("refedit.hide",icon='EYEDROPPER',text='',emboss = False)

        box = layout.box()
        box.alignment='EXPAND'

        RefEditor = context.scene.RefEditor

        col = layout.column(align=True)

        if RefEditor.get('objects') :

            for key in sorted(RefEditor['objects']):

                object = pathConvert(key)
                depth = key.split('/')[1:]

                if RefEditor['objects'][key]['expand'] == True :
                    expandIcon = 'DISCLOSURE_TRI_DOWN'
                else :
                    expandIcon = 'DISCLOSURE_TRI_RIGHT'

                if RefEditor['objects'][key]['hide'] == False :
                    hideIcon ='RESTRICT_VIEW_OFF'
                else :
                    hideIcon ='RESTRICT_VIEW_ON'

                ExcludeObject=[]
                ChildObject = []

                if RefEditor.search.lower() not in object.name.lower() :
                    ExcludeObject.append(key)

                for otherKey,value in RefEditor['objects'].items() :
                    if otherKey != key and key.startswith(otherKey) and RefEditor['objects'][otherKey]['expand']==False :
                        ExcludeObject.append(key)

                    if otherKey != key and otherKey.startswith(key):
                        ChildObject.append(otherKey)

                if RefEditor.filterSelect == True and object not in context.selected_objects :
                    ExcludeObject.append(key)

                if key not in ExcludeObject :

                    row = col.row(align=True)

                    for i in range(1, len(depth)) :
                        row.separator()

                    if  not ChildObject:
                        row.label(icon ='LAYER_USED')
                    else :
                        row.operator("refedit.expand",emboss = False,icon=expandIcon,text='').object =key

                    row.label(object.name)
                    row.operator("refedit.create_proxy",emboss = False,icon='EMPTY_DATA',text ='').object = key
                    row.operator("refedit.hide",emboss = False,icon=hideIcon,text ='').object =key

cls = [refEditorSettings,RefEditorHide,RefEditorExpand,RefEditorPanel,RefEditorCreateProxy]

@persistent
def my_handler(dummy):
    link_analyser(bpy.context.scene.objects,'')

def register():
    for c in cls :
        bpy.utils.register_class(c)

    bpy.types.Scene.RefEditor= bpy.props.PointerProperty(type = refEditorSettings)
    bpy.app.handlers.load_post.append(my_handler)

def unregister():
    for c in cls :
        bpy.utils.unregister_class(c)

    del bpy.types.Scene.RefEditor
    bpy.app.handlers.load_post.remove(my_handler)


#Paso 1: Pedir la edad de la persona 1
edadPersona1 = input('Por favor, ingrese la edad de la primera persona: ')

#Paso 2: Pedir la edad de la persona 2
edadPersona2 = input('Por favor, ingrese la edad de la segunda persona: ')
#inputNumerico = int(input('Por favor, ingrese la edad de la segunda persona: '))

# Paso 3: Calcular la diferencia de las edades
#nota: el ingreso de input es string, para hacer operaciones matematicas hay que cambiarla
diferencia = int(edadPersona1) - int(edadPersona2)
# Paso 4: Imprimir diferencia de las edades
print(f'La diferencia de las edades es {diferencia} años')


#-*- coding:utf-8 -*-
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

#设置所使用的字体
font = ImageFont.truetype("/Library/Fonts/Microsoft/Consolas.ttf", 90)

#打开图片
imageFile = "/Users/mac/Desktop/IMG_20161223_090222.jpg"
im1 = Image.open(imageFile)

#画图
draw = ImageDraw.Draw(im1)
draw.text((100, 3600), "Only use for qiniu yun", (255, 255, 255), font=font)    #设置文字位置/内容/颜色/字体
draw = ImageDraw.Draw(im1)                          #Just draw it!

#另存图片
im1.save("/Users/mac/Desktop/target1.jpg")


from django.http import HttpResponse
import json, urllib.request, datetime

def IaaS_API(request):
    instanceNum = ''
    time = timearr()
    uri = {
        'server' : {
            # getServerImageProductList ( 서버이미지 상품 리스트 조회 )
            "1" : 'getServerImageProductList',
            # getServerProductList ( 서버 상품 리스트 조회 ) + serverImageProductCode 쿼리 필수
            "2" : 'getServerProductList' + '?serverImageProductCode=SPSW0LINUX000031',
            # getRaidList ( RAID 리스트 조회 )
            "3" : 'getRaidList',
            # getZoneList ( ZONE 리스트 조회 )
            "4" : 'getZoneList',
            # getRegionList ( REGION 리스트 조회)
            "5" : 'getRegionList',
            # getNasVolumeInstanceList ( NAS 볼륨 인스턴스 리스트 조회 )
            "6" : 'getNasVolumeInstanceList',
            # getAccessControlGroupList( 접근제어 그룹 리스트 조회)
            "7" : 'getAccessControlGroupList',
            # getAccessControlGroupServerInstanceList ( 접근제어 그룹 적용된 서버 인스턴스 리스트 조회 ) + accessControlGroupConfigurationNo 쿼리 필수
            "8" : 'getAccessControlGroupServerInstanceList' + "?accessControlGroupConfigurationNo=" + instanceNum,
            # getAccessControlRuleList ( 접근제어 규칙 리스트 조회 ) + accessControlGroupConfigurationNo 쿼리 필수
            "9" : 'getAccessControlRuleList' + '?accessControlGroupConfigurationNo=' + instanceNum,
            # getServerInstanceList ( 서버 인스턴스 리스트 조회 )
            "10" : 'getServerInstanceList',
            # getMemberServerImageList ( 회원 서버 이미지 리스트 조회 )
            "11" : 'getMemberServerImageList',
            # getBlockStorageInstanceList ( 블록 스토리지 인스턴스 리스트 조회 )
            "12" : 'getBlockStorageInstanceList',
            # getBlockStorageSnapshotInstanceList ( 블록 스토리지 스냅샷 인스턴스 리스트 조회 )
            "13" : 'getBlockStorageSnapshotInstanceList',
            # getPublicIpInstanceList ( 공인 IP 인스턴스 리스트 조회 )
            "14" : 'getPublicIpInstanceList',
            # getPortForwardingRuleList ( 포트 포워딩 Rule 리스트 조회)
            "15" : 'getPortForwardingRuleList'
        },
        'lb' : {
            # LB 인스턴스 리스트
            '1' : 'getLoadBalancerInstanceList',
            # LB SSL 인증서조회
            '2' : 'getLoadBalancerSslCertificateList'
        },
        'autoscaling' : {
            # 론치설정 리스트
            '1' : 'getLaunchConfigurationList',
            # 그룹 리스트
            '2' : 'getAutoScalingGroupList',
            # 스케쥴액션 리스트
            '3' : 'getScheduledActionList',
            # 프로세스 구분 리스트
            '4' : 'getScalingProcessTypeList',
            # 액티비티 로그 리스트
            '5' : 'getAutoScalingActivityLogList',
            # 오토스케일링 설정 로그 리스트
            '6' : 'getAutoScalingConfigurationLogList',
            # 오토스케일링 정책 리스트
            '7' : 'getAutoScalingPolicyList',
            # 조정유형 리스트
            '8' : 'getAdjustmentTypeList'
        },
        'monitoring' : {
            # Metric별 통계 정보 조회
            '1' : 'getMetricStatistics?' + 'instanceNoList.1=' + instanceNum + '&metricName=CPUUtilization&startTime='+ time[0] +'&endTime='+ time[1] +'&period=1800',
            # Metric 리스트 조회
            '2' : 'getListMetrics?' + 'instanceNo=' + instanceNum
        },
        'security' : {
            # '1' : 'getAppInstanceStatistics' + '?appInstanceNo='
        },
        'cdn' : {
            # CDN+ 인스턴스리스트
            '1' : 'getCdnPlusInstanceList',
            # Global CDN 인스턴스리스트
            '2' : 'getGlobalCdnInstanceList'
        },
        'clouddb' : {
            # CloudDB Config group 조회
            '1' : 'getCloudDBConfigGroupList?dbKindCode=MYSQL',
            # CloudDB 상품 리스트
            '2' : 'getCloudDBImageProductList?dbKindCode=MYSQL',
            # CloudDB 이미지 상품 리스트
            # '3' : 'getCloudDBProductListRequest' + '?'
        }
    }
    if request==0:
        return uri
    else:
        return HttpResponse(json.dumps(uri), content_type='application/json')

def PaaS_API(request):
    uri = {
    # /geolocation/v1/
        'geolocation' : {
            '1' : 'geoLocation?ip=202.131.30.11'
        },
        'mailer': {
            '1' : 'mails/requests/20181022000000073804/status'
        }
    }
    if request==0:
        return uri
    else:
        return HttpResponse(json.dumps(uri), content_type='application/json')

def App_API(request):
    uri = {
        'clova' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/voice/v1/tts'
        },
        'maps' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/map/v1/geocode?query=%EB%B6%88%EC%A0%95%EB%A1%9C%206'
        },
        'papago' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/smt/v1/translation',
            '2' : 'https://naveropenapi.apigw.ntruss.com/nmt/v1/translation'
        },
        'nshorturl' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/util/v1/shorturl',
            #'2' : 'https://naveropenapi.apigw.ntruss.com/util/v1/shorturl?url=http://d2.naver.com/helloworld/4874130'
        },
        'captcha' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/captcha/v1/nkey?code=0',
            '2' : 'https://naveropenapi.apigw.ntruss.com/scaptcha/v1/skey?code=0'
        },
        'searchtrend' : {
            '1' : 'https://naveropenapi.apigw.ntruss.com/datalab/v1/search'
        },
        'apigateway' : {
            '1' : 'https://ta50do4nv5.apigw.ntruss.com/api-test/v1/api'
        }
    }
    if request == 0:
        return uri
    else:
        return HttpResponse(json.dumps(uri), content_type='application/json')

def timearr():
    ntime = datetime.datetime.today().strftime("%Y-%m-%dT%H:%M:%SZ")
    ntime = list(ntime)
    hh = int(datetime.datetime.today().strftime("%H"))
    btime = list(ntime)
    if hh == 0:
        nh = '00'
        bh = '23'
        bd = int(datetime.datetime.today().strftime("%d")) - 1
        if bd == 0 :
            month = int(datetime.datetime.today().strftime("%m")) - 1
            if month==4 or month==6 or month==9 or month==11:
                bd = '30'
            elif month == 2:
                year = int(datetime.datetime.today().strftime("%Y"))
                if (year%4==0 and y%100 !=0) or year % 400 == 0:
                    bd = '29'
                else :
                    bd = '28'
            else:
                bd = '31'
            btime[5:7] = repr(month)
        elif bd < 10:
            bd = '0' + repr(bd)
        else:
            bd = repr(bd)
        btime[8:10] = bd

    elif hh < 10:
        nh = '0' + repr(hh)
        bh = '0' + repr(int(hh) - 1)

    else:
        nh = repr(hh)
        if int(hh) <= 10:
            bh = '0' + repr(int(hh) - 1)
        else:
            bh = repr(int(hh) - 1)

    ntime[11:13] = nh
    ntime = ''.join(ntime)
    btime[11:13] = bh
    btime = ''.join(btime)

    return [btime, ntime]

def papagoDict():
    papagoDict.LangText = {
        'ko' : '안녕하세요',
        'ja' : 'お会いできて嬉しいです',
        'en' : 'Nice to meet you',
        'zh-CN' : '很高兴见到你',
        'zh-TW' : '很高興見到你',
        'es' : 'Encantado de conocerle',
        'fr' : 'Je suis content de vous rencontrer',
        'vi' : 'Rất vui được gặp các bạn',
        'th' : 'ยินดีที่ได้พบคุณ',
        'id' : 'Senang bertemu dengan Anda',
    }
    papagoDict.SmtArr = {
        'ko' : ['en','ja', 'zh-CN'],
        'en' : ['ko'],
        'ja' : ['ko'],
        'zh-CN' : ['ko'],
    }
    papagoDict.NmtArr = {
        # ko<->en, ko <-> zh-CN, ko <-> zh-TW, ko<->es, ko<->fr, ko<->vi, ko<->th, ko<->id, en<->ja, en<->fr 조합만 가능
        'ko' : ['en', 'zh-CN', 'zh-TW', 'es', 'fr', 'vi', 'th', 'id'],
        'en' : ['ko', 'ja', 'fr'],
        'zh-CN' : ['ko'],
        'zh-TW' : ['ko'],
        'es' : ['ko'],
        'fr' : ['ko', 'en'],
        'vi' : ['ko'],
        'th' : ['ko'],
        'id' : ['ko'],
        'ja' : ['en'],
    }


import Pyro4

@Pyro4.expose

class HelloWorldMaker(object):
	def get_message(self,name):
		print("CALL: get message method") #Log point
		return "Hello world {0}\n"\
			"This message is coming from the sever".format(name)

	def sum_of_2(self, a, b):
		print("CALL: sum_of_2 method")
		return int(a) + int(b)

daemon = Pyro4.Daemon()

uri = daemon.register(HelloWorldMaker)

print("Ready. Object uri =",uri)
daemon.requestLoop()


import numpy as np
import cv2
import math

class Square(object):
    def __init__(self, parent=None, position=None):
        self.parent = parent
        self.position = position
        self.isWall = False
        # Sets a percentage of the squares to be walls at random
        if np.random.rand(1) < 0.5:
            self.isWall = True
        self.f = 0
        self.g = 0
        self.h = 0

    def __eq__(self,other):
        # We evaluate whether squares are equivalent based on their positioning
        return self.position == other.position

def createBoard(length,squares,start,end):
    # Create the start and end squares, make sure they arent walls
    start = Square(None, start)
    start.isWall = False
    start.g=start.h=start.f=0
    end = Square(None, end)
    end.isWall = False
    end.g=end.f=end.h=0

    # create the board with a white background
    board = np.zeros((length,length,3),np.uint8)
    squareLength = length / squares
    board = cv2.rectangle(board,(0,0),(length,length),(255,255,255),-1)
    count = 0
    walls = []
    for row in range(squares):
        for col in range(squares):
            # create a square for each grid location on the board
            r = Square(None,(row,col))
            # Color the start blue
            if r == start:
                board = cv2.rectangle(board,(int(row*squareLength),int(col*squareLength)),
                                    (int(row*squareLength + squareLength),int(col*squareLength + squareLength)),(255,100,100),-1)
                count += 1
            # Color the end red
            elif r == end:
                board = cv2.rectangle(board,(int(row*squareLength),int(col*squareLength)),
                                    (int(row*squareLength + squareLength),int(col*squareLength + squareLength)),(100,100,255),-1)
                count += 1
            # Draw a thin black border around the open squares
            elif r.isWall == False:
                board = cv2.rectangle(board,(int(row*squareLength),int(col*squareLength)),
                                    (int(row*squareLength + squareLength),int(col*squareLength + squareLength)),(0,0,0),1)
                count += 1
            # Draw the walls in black
            elif r.isWall == True:
                board = cv2.rectangle(board,(int(row*squareLength),int(col*squareLength)),
                                    (int(row*squareLength + squareLength),int(col*squareLength + squareLength)),(0,0,0),-1)
                count += 1
                # Keep track of the walls
                walls.append(r)
    if count == squares**2:
        print("Made the board")
        return board, walls, start, end

def findPath(board, walls, squares, start, end):
    openSet = []
    closedSet = []
    openSet.append(start)
    print("starting to find path")
    while openSet:
        currentNode = openSet[0]
        currentIndex = 0
        # Look through openSet to find the square with the greatest f value
        for index, node in enumerate(openSet):
            if node.f < currentNode.f:
                currentNode = node
                currentIndex = index
        openSet.pop(currentIndex)
        closedSet.append(currentNode)

        children = []
        if currentNode == end:
            print("Found the end")
            path = []
            current = currentNode
            # If we are at the end, look at the squares parents to find the optimal path
            while current is not None:
                path.append(current.position)
                current = current.parent
            # This returns the ideal path through the maze, excluding the start and end points
            return path[-2:0:-1]

        # look through all of the squares around the current square
        for newPosition in [(1,1),(1,0),(0,1),(1,-1),(-1,1),(0,-1),(-1,0),(-1,-1)]:
            nodePosition = (currentNode.position[0]+newPosition[0],currentNode.position[1]+newPosition[1])

            # Check to see if the new position is in the board
            if nodePosition[0] > (squares-1) or nodePosition[0] < 0 or nodePosition[1] > (squares-1) or nodePosition[1] < 0:
                continue

            # If an adjacent square passes the two tests, make it a child of the currentNode
            newNode = Square(currentNode, nodePosition)

            # If the currentNode is a wall, ignore it and move on
            if newNode in walls:
                continue

            children.append(newNode)

        for child in children:
            # If the child is in closedSet, we skip it
            if child in closedSet:
                continue

            # Heuristic takes the euclidian distance to select the square closest to the end
            child.g = currentNode.g + 1
            child.h = math.dist((child.position[0],child.position[1]),(end.position[0],end.position[1]))
            child.f = child.g + child.h

            # Dont think that this is the cleanest way to do this but I think it works
            # If the child is in openSet and its g value is greater than openNode, add it to closed set
            # Then check again to see if the child is in closedSet, and pass if it is
            for openNode in openSet:
                if child == openNode and child.g > openNode.g:
                    closedSet.append(child)
            if child in closedSet:
                continue

            # If the child passes the above tests, add it to open set
            openSet.append(child)

def drawSearch(board,squareLength,path):
    # This function takes the path that was calculated and draws it out on the board
    for square in path:
        board = cv2.rectangle(board,(int(square[0]*squareLength+ squareLength/4),int(square[1]*squareLength+squareLength/4)),
                            (int(square[0]*squareLength + 3*squareLength/4),int(square[1]*squareLength + 3*squareLength/4)),(255,255,0),-1)
    return board

def run(length,squares):
    squareLength = length/squares
    start = (0,0)
    end = (squares-1,squares-1)
    board, walls, start, end = createBoard(length,squares,start,end)
    try:
        path = findPath(board,walls,squares,start,end)
        print(path)
        board = drawSearch(board,squareLength,path)
    except:
        print('There is no solution to this puzzle')

    cv2.imshow('Howdy',board)
    cv2.waitKey(0)

if __name__ == "__main__":
    run(500,50)


# TODO: drop to PDB option
# TODO: iteration count debug print seems one higher?

# *** Not prioritized for v0 ***
# TODO: increase test coverage: TypeVar('T', int, str) vs bounded type vars
# TODO: consider raises conditions (guaranteed to raise, guaranteed to not raise?)
# TODO: precondition strengthening ban (Subclass constraint rule)
# TODO: double-check counterexamples
# TODO: mutating symbolic Callables?
# TODO: contracts on the contracts of function and object inputs/outputs?

from dataclasses import dataclass, replace
import collections
from contextlib import ExitStack
import copy
import enum
import inspect
from inspect import BoundArguments
from inspect import Signature
import itertools
import functools
import linecache
import os.path
import sys
import time
import traceback
import types
from typing import *
import typing

import typing_inspect  # type: ignore
import z3  # type: ignore

from crosshair import dynamic_typing

from crosshair.codeconfig import collect_options
from crosshair.condition_parser import condition_parser
from crosshair.condition_parser import get_current_parser
from crosshair.condition_parser import Conditions
from crosshair.condition_parser import ConditionExpr
from crosshair.condition_parser import ConditionExprType
from crosshair.condition_parser import UNABLE_TO_REPR

from crosshair.enforce import EnforcedConditions
from crosshair.enforce import NoEnforce
from crosshair.enforce import WithEnforcement
from crosshair.enforce import PreconditionFailed
from crosshair.enforce import PostconditionFailed
from crosshair.fnutil import resolve_signature
from crosshair.options import AnalysisOptions
from crosshair.options import AnalysisOptionSet
from crosshair.options import DEFAULT_OPTIONS
from crosshair.statespace import context_statespace
from crosshair.statespace import optional_context_statespace
from crosshair.statespace import prefer_true
from crosshair.statespace import AnalysisMessage
from crosshair.statespace import CallAnalysis
from crosshair.statespace import MessageType
from crosshair.statespace import SinglePathNode
from crosshair.statespace import SimpleStateSpace
from crosshair.statespace import StateSpace
from crosshair.statespace import StateSpaceContext
from crosshair.statespace import VerificationStatus
from crosshair.fnutil import FunctionInfo
from crosshair.tracers import COMPOSITE_TRACER
from crosshair.tracers import NoTracing
from crosshair.tracers import PatchingModule
from crosshair.tracers import ResumedTracing
from crosshair.tracers import TracingModule
from crosshair.tracers import TracingOnly
from crosshair.tracers import is_tracing
from crosshair.type_repo import get_subclass_map
from crosshair.util import debug
from crosshair.util import frame_summary_for_fn
from crosshair.util import name_of_type
from crosshair.util import samefile
from crosshair.util import smtlib_typename
from crosshair.util import sourcelines
from crosshair.util import test_stack
from crosshair.util import AttributeHolder
from crosshair.util import CrosshairInternal
from crosshair.util import CrosshairUnsupported
from crosshair.util import DynamicScopeVar
from crosshair.util import IgnoreAttempt
from crosshair.util import UnexploredPath

_MISSING = object()

_OPCODE_PATCHES: List[TracingModule] = []

_PATCH_REGISTRATIONS: Dict[Callable, Callable] = {}

class Patched(TracingModule):
    def __enter__(self):
        ptchs = {}
        for idwrapper, callable in _PATCH_REGISTRATIONS.items():
            ptchs[idwrapper] = callable
        COMPOSITE_TRACER.push_module(PatchingModule(ptchs))
        push_count = 1
        if len(_OPCODE_PATCHES) == 0:
            raise CrosshairInternal("Opcode patches haven't been loaded yet.")
        for module in _OPCODE_PATCHES:
            COMPOSITE_TRACER.push_module(module)
            push_count += 1
        self.push_count = push_count
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        for _ in range(self.push_count):
            COMPOSITE_TRACER.pop_config()
        return False

class _StandaloneStatespace(ExitStack):
    def __enter__(self):
        # We explicitly don't set up contexts to enforce conditions - that's because
        # conditions involve a choice, and standalone_statespace is for testing that
        # does not require making any choices.
        super().__enter__()
        space = SimpleStateSpace()
        self.enter_context(condition_parser(DEFAULT_OPTIONS.analysis_kind))
        self.enter_context(Patched())
        self.enter_context(StateSpaceContext(space))
        self.enter_context(COMPOSITE_TRACER)
        COMPOSITE_TRACER.trace_caller()
        return space

standalone_statespace = _StandaloneStatespace()

class ExceptionFilter:
    analysis: CallAnalysis
    ignore: bool = False
    ignore_with_confirmation: bool = False
    user_exc: Optional[Tuple[Exception, traceback.StackSummary]] = None
    expected_exceptions: Tuple[Type[BaseException], ...]

    def __init__(
        self, expected_exceptions: FrozenSet[Type[BaseException]] = frozenset()
    ):
        self.expected_exceptions = (NotImplementedError,) + tuple(expected_exceptions)

    def has_user_exception(self) -> bool:
        return self.user_exc is not None

    def __enter__(self) -> "ExceptionFilter":
        if not is_tracing():
            raise CrosshairInternal("must be tracing during exception filter")
        return self

    def __exit__(self, exc_type, exc_value, tb) -> bool:
        with NoTracing():
            if isinstance(exc_value, (PostconditionFailed, IgnoreAttempt)):
                if isinstance(exc_value, PostconditionFailed):
                    # Postcondition : although this indicates a problem, it's with a
                    # subroutine; not this function.
                    # Usualy we want to ignore this because it will be surfaced more locally
                    # in the subroutine.
                    debug(
                        f"Ignoring based on internal failed post condition: {exc_value}"
                    )
                self.ignore = True
                self.analysis = CallAnalysis()
                return True
            if isinstance(exc_value, self.expected_exceptions):
                exc_type_name = type(exc_value).__name__
                debug(f"Hit expected exception: {exc_type_name}: {exc_value}")
                self.ignore = True
                self.analysis = CallAnalysis(VerificationStatus.CONFIRMED)
                return True
            if isinstance(exc_value, TypeError):
                exc_str = str(exc_value)
                if (
                    "SymbolicStr" in exc_str
                    or "SymbolicInt" in exc_str
                    or "SymbolicFloat" in exc_str
                    or "__hash__ method should return an integer" in exc_str
                    or "expected string or bytes-like object" in exc_str
                ):
                    # Ideally we'd attempt literal strings after encountering this.
                    # See https://github.com/pschanely/CrossHair/issues/8
                    debug("Proxy intolerace at: ", traceback.format_exc())
                    raise CrosshairUnsupported("Detected proxy intolerance: " + exc_str)
            if isinstance(
                exc_value, (UnexploredPath, CrosshairInternal, z3.Z3Exception)
            ):
                return False  # internal issue: re-raise
            if isinstance(exc_value, BaseException):
                # Most other issues are assumed to be user-facing exceptions:
                self.user_exc = (exc_value, traceback.extract_tb(sys.exc_info()[2]))
                self.analysis = CallAnalysis(VerificationStatus.REFUTED)
                return True  # suppress user-level exception
            return False  # re-raise resource and system issues

_T = TypeVar("_T")

from crosshair.tracers import NoTracing

def realize(value: _T) -> _T:
    with NoTracing():
        if hasattr(type(value), "__ch_realize__"):
            return value.__ch_realize__()  # type: ignore
        else:
            return value

_INSIDE_REALIZATION = DynamicScopeVar(bool, "inside_realization")

def inside_realization() -> bool:
    return _INSIDE_REALIZATION.get(default=False)

# TODO: some kind of comprehensive realization tests.
def deep_realize(value: _T) -> _T:
    with NoTracing():
        with _INSIDE_REALIZATION.open(True):
            try:
                return copy.deepcopy(value, {})
            except TypeError as exc:
                debug(f"abort realizing {type(value)} object: {type(exc)}: {exc}")
                return value

class CrossHairValue:
    def __deepcopy__(self, memo: Dict) -> object:
        if inside_realization() and hasattr(self, "__ch_realize__"):
            result = copy.deepcopy(self.__ch_realize__())  # type: ignore
        else:
            # Try to replicate the regular deepcopy:
            cls = self.__class__
            result = cls.__new__(cls)
            for k, v in self.__dict__.items():
                object.__setattr__(result, k, copy.deepcopy(v, memo))
        memo[id(self)] = result
        return result

def normalize_pytype(typ: Type) -> Type:
    if typing_inspect.is_typevar(typ):
        # we treat type vars in the most general way possible (the bound, or as 'object')
        bound = typing_inspect.get_bound(typ)
        if bound is not None:
            return normalize_pytype(bound)
        constraints = typing_inspect.get_constraints(typ)
        if constraints:
            raise CrosshairUnsupported
            # TODO: not easy; interpreting as a Union allows the type to be
            # instantiated differently in different places. So, this doesn't work:
            # return Union.__getitem__(tuple(map(normalize_pytype, constraints)))
        return object
    if typ is Any:
        # The distinction between any and object is for type checking, crosshair treats them the same
        return object
    if typ is Type:
        return type
    return typ

def origin_of(typ: Type) -> Type:
    if hasattr(typ, "__origin__"):
        return typ.__origin__
    return typ

def type_arg_of(typ: Type, index: int) -> Type:
    args = type_args_of(typ)
    return args[index] if index < len(args) else object

def type_args_of(typ: Type) -> Tuple[Type, ...]:
    if getattr(typ, "__args__", None):
        return typing_inspect.get_args(typ, evaluate=True)
    else:
        return ()

def python_type(o: object) -> Type:
    if is_tracing():
        raise CrosshairInternal("should not be tracing while getting pytype")
    if hasattr(type(o), "__ch_pytype__"):
        obj_type = o.__ch_pytype__()  # type: ignore
        if hasattr(obj_type, "__origin__"):
            obj_type = obj_type.__origin__
        return obj_type
    else:
        return type(o)

def with_realized_args(fn: Callable) -> Callable:
    def realizer(*a, **kw):
        a = map(realize, a)
        kw = {k: realize(v) for (k, v) in kw.items()}
        return fn(*a, **kw)

    functools.update_wrapper(realizer, fn)
    return realizer

_IMMUTABLE_TYPES = (int, float, complex, bool, tuple, frozenset, type(None))

def choose_type(space: StateSpace, from_type: Type) -> Type:
    subtypes = get_subclass_map()[from_type]
    # Note that this is written strangely to leverage the default
    # preference for false when forking:
    if not subtypes or not space.smt_fork(desc="choose_" + smtlib_typename(from_type)):
        return from_type
    for subtype in subtypes[:-1]:
        if not space.smt_fork(desc="choose_" + smtlib_typename(subtype)):
            return choose_type(space, subtype)
    return choose_type(space, subtypes[-1])

def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
    # pydantic sets __signature__ on the class, so we look for that as well as on
    # __init__ (see https://github.com/samuelcolvin/pydantic/pull/1034)
    if hasattr(cls, "__signature__"):
        sig = resolve_signature(cls)
        if isinstance(sig, inspect.Signature):
            return sig
    new_fn = cls.__new__
    init_fn = cls.__init__
    if (
        new_fn is not object.__new__
        and
        # Some superclasses like Generic[T] define __new__ with typless (*a,**kw)
        # args. Skip if we don't have types on __new__.
        # TODO: merge the type signatures of __init__ and __new__, pulling the
        # most specific types from each.
        len(get_type_hints(new_fn)) > 0
    ):
        sig = resolve_signature(new_fn)
    elif init_fn is not object.__init__:
        sig = resolve_signature(init_fn)
    else:
        return inspect.Signature([])
    if isinstance(sig, inspect.Signature):
        # strip first argument
        newparams = list(sig.parameters.values())[1:]
        return sig.replace(parameters=newparams)
    return None

def proxy_for_class(typ: Type, varname: str) -> object:
    data_members = get_type_hints(typ)
    class_conditions = get_current_parser().get_class_conditions(typ)
    has_invariants = class_conditions is not None and bool(class_conditions.inv)

    # Special handling for some magical types:
    if issubclass(typ, tuple):
        tuple_args = {
            k: proxy_for_type(t, varname + "." + k) for (k, t) in data_members.items()
        }
        return typ(**tuple_args)  # type: ignore
    elif sys.version_info >= (3, 8) and type(typ) is typing._TypedDictMeta:  # type: ignore
        # Handling for TypedDict
        optional_keys = getattr(typ, "__optional_keys__", ())
        keys = (
            k
            for k in data_members.keys()
            if k not in optional_keys or context_statespace().smt_fork()
        )
        return {k: proxy_for_type(data_members[k], varname + "." + k) for k in keys}

    constructor_sig = get_constructor_signature(typ)
    if constructor_sig is None:
        raise CrosshairUnsupported(
            f"unable to create concrete instance of {typ} due to bad constructor"
        )
    args = gen_args(constructor_sig)
    try:
        with ResumedTracing():
            obj = WithEnforcement(typ)(*args.args, **args.kwargs)
    except (PreconditionFailed, PostconditionFailed):
        # preconditions can be invalidated when the __init__ method has preconditions.
        # postconditions can be invalidated when the class has invariants.
        raise IgnoreAttempt
    except BaseException as e:
        debug("Root-cause type construction traceback:", test_stack(e.__traceback__))
        raise CrosshairUnsupported(
            f"error constructing {name_of_type(typ)} instance: {name_of_type(type(e))}: {e}",
        ) from e

    debug("Proxy as a concrete instance of", name_of_type(typ))
    return obj

def register_patch(entity: Callable, patch_value: Callable):
    if entity in _PATCH_REGISTRATIONS:
        raise CrosshairInternal(f"Doubly registered patch: {entity}")
    _PATCH_REGISTRATIONS[entity] = patch_value

def register_opcode_patch(module: TracingModule) -> None:
    _OPCODE_PATCHES.append(module)

class SymbolicFactory:
    """
    A callable object that creates symbolic values.

    .. automethod:: __call__
    """

    def __init__(self, space: StateSpace, pytype: object, varname: str):
        self.space = space
        self.pytype: Any = pytype
        self.varname = varname

    @overload
    def __call__(
        self, typ: Callable[..., _T], suffix: str = "", allow_subtypes: bool = True
    ) -> _T:
        ...

    @overload
    def __call__(self, typ: Any, suffix: str = "", allow_subtypes: bool = True) -> Any:
        ...

    def __call__(self, typ, suffix: str = "", allow_subtypes: bool = True):
        """
        Create a new symbolic value.

        :param typ: The corresponding Python type for the returned symbolic.
        :type typ: type
        :param suffix: A descriptive suffix used to name variable(s) in the solver.
        :type suffix: str
        :param allow_subtypes: Whether it's ok to return a subtype of given type.
        :type allow_subtypes: bool
        :returns: A new symbolic value.
        """
        return proxy_for_type(
            typ,
            self.varname + suffix + self.space.uniq(),
            allow_subtypes=allow_subtypes,
        )

_SIMPLE_PROXIES: MutableMapping[object, Callable] = {}

SymbolicCreationCallback = Union[
    # Sadly Callable[] doesn't support variable arguments. Just enumerate:
    Callable[[SymbolicFactory], object],
    Callable[[SymbolicFactory, Type], object],
    Callable[[SymbolicFactory, Type, Type], object],
    Callable[[SymbolicFactory, Type, Type, Type], object],
    Callable[[SymbolicFactory, Type, Type, Type, Type], object],
]

def register_type(typ: Type, creator: SymbolicCreationCallback) -> None:
    """
    Register a custom creation function to create symbolic values for a type.

    :param typ: The Python type (or typing annotation) to handle.
    :param creator: A function that takes a :class:`SymbolicFactory` instance and
      returns a symbolic value. When creating a parameterized type (e.g. List[int]),
      type parameters will be given to `creator` as additional arguments following the
      factory.
    """
    assert typ is origin_of(
        typ
    ), f'Only origin types may be registered, not "{typ}": try "{origin_of(typ)}" instead.'
    if typ in _SIMPLE_PROXIES:
        raise CrosshairInternal(f'Duplicate type "{typ}" registered')
    _SIMPLE_PROXIES[typ] = creator

@overload
def proxy_for_type(
    typ: Callable[..., _T],
    varname: str,
    allow_subtypes: bool = True,
) -> _T:
    ...

@overload
def proxy_for_type(
    typ: Any,
    varname: str,
    allow_subtypes: bool = True,
) -> Any:
    ...

def proxy_for_type(
    typ: Any,
    varname: str,
    allow_subtypes: bool = False,
) -> Any:
    space = context_statespace()
    with NoTracing():
        typ = normalize_pytype(typ)
        origin = origin_of(typ)
        type_args = type_args_of(typ)
        # special cases
        if isinstance(typ, type) and issubclass(typ, enum.Enum):
            enum_values = list(typ)  # type:ignore
            if not enum_values:
                raise IgnoreAttempt("No values for enum")
            for enum_value in enum_values[:-1]:
                if space.smt_fork(desc="choose_enum_" + str(enum_value)):
                    return enum_value
            return enum_values[-1]
        # It's easy to forget to import crosshair.core_and_libs; check:
        assert _SIMPLE_PROXIES, "No proxy type registrations exist"
        proxy_factory = _SIMPLE_PROXIES.get(origin)
        if proxy_factory:
            recursive_proxy_factory = SymbolicFactory(space, typ, varname)
            return proxy_factory(recursive_proxy_factory, *type_args)
        if allow_subtypes and typ is not object:
            typ = choose_type(space, typ)
        return proxy_for_class(typ, varname)

def gen_args(sig: inspect.Signature) -> inspect.BoundArguments:
    if is_tracing():
        raise CrosshairInternal
    args = sig.bind_partial()
    space = context_statespace()
    for param in sig.parameters.values():
        smt_name = param.name + space.uniq()
        proxy_maker = lambda typ: proxy_for_type(typ, smt_name, allow_subtypes=True)
        has_annotation = param.annotation != inspect.Parameter.empty
        value: object
        if param.kind == inspect.Parameter.VAR_POSITIONAL:
            if has_annotation:
                varargs_type = List[param.annotation]  # type: ignore
                value = proxy_maker(varargs_type)
            else:
                value = proxy_maker(List[Any])
        elif param.kind == inspect.Parameter.VAR_KEYWORD:
            if has_annotation:
                varargs_type = Dict[str, param.annotation]  # type: ignore
                value = cast(dict, proxy_maker(varargs_type))
                # Using ** on a dict requires concrete string keys. Force
                # instiantiation of keys here:
                value = {k.__str__(): v for (k, v) in value.items()}
            else:
                value = proxy_maker(Dict[str, Any])
        else:
            is_self = param.name == "self"
            # Object parameters can be any valid subtype iff they are not the
            # class under test ("self").
            allow_subtypes = not is_self
            if has_annotation:
                value = proxy_for_type(param.annotation, smt_name, allow_subtypes)
            else:
                value = proxy_for_type(cast(type, Any), smt_name, allow_subtypes)
        debug("created proxy for", param.name, "as type:", name_of_type(type(value)))
        args.arguments[param.name] = value
    return args

def message_sort_key(m: AnalysisMessage) -> tuple:
    return (m.state, UNABLE_TO_REPR not in m.message, -len(m.message))

class MessageCollector:
    def __init__(self):
        self.by_pos = {}

    def extend(self, messages: Iterable[AnalysisMessage]) -> None:
        for message in messages:
            self.append(message)

    def append(self, message: AnalysisMessage) -> None:
        key = (message.filename, message.line, message.column)
        if key in self.by_pos:
            self.by_pos[key] = max(self.by_pos[key], message, key=message_sort_key)
        else:
            self.by_pos[key] = message

    def get(self) -> List[AnalysisMessage]:
        return [m for (k, m) in sorted(self.by_pos.items())]

class Checkable:
    def analyze(self) -> Iterable[AnalysisMessage]:
        raise NotImplementedError

@dataclass
class ConditionCheckable(Checkable):
    ctxfn: FunctionInfo
    options: AnalysisOptions
    conditions: Conditions

    def analyze(self) -> Iterable[AnalysisMessage]:
        options = self.options
        conditions = self.conditions
        debug('Analyzing postcondition: "', conditions.post[0].expr_source, '"')
        debug(
            "assuming preconditions: ",
            ",".join([p.expr_source for p in conditions.pre]),
        )
        options.deadline = time.monotonic() + options.per_condition_timeout

        with condition_parser(options.analysis_kind):
            analysis = analyze_calltree(options, conditions)

        (condition,) = conditions.post
        if analysis.verification_status is VerificationStatus.UNKNOWN:
            message = "Not confirmed."
            analysis.messages = [
                AnalysisMessage(
                    MessageType.CANNOT_CONFIRM,
                    message,
                    condition.filename,
                    condition.line,
                    0,
                    "",
                )
            ]
        elif analysis.verification_status is VerificationStatus.CONFIRMED:
            message = "Confirmed over all paths."
            analysis.messages = [
                AnalysisMessage(
                    MessageType.CONFIRMED,
                    message,
                    condition.filename,
                    condition.line,
                    0,
                    "",
                )
            ]

        return analysis.messages

class ClampedCheckable(Checkable):
    """
    Clamp messages for a class method to appear on the class itself.

    So, even if the method is defined on a superclass, or defined dynamically (via
    decorator etc), we report it on the class definition instead.
    """

    def __init__(self, checkable: Checkable, cls: type):
        self.checkable = checkable
        filename, start_line, _ = sourcelines(cls)
        self.cls_file = filename
        self.cls_start_line = start_line

    def analyze(self) -> Iterable[AnalysisMessage]:
        cls_file = self.cls_file
        ret = []
        for message in self.checkable.analyze():
            if not samefile(message.filename, cls_file):
                ret.append(
                    replace(message, filename=cls_file, line=self.cls_start_line)
                )
            else:
                ret.append(message)
        return ret

@dataclass
class SyntaxErrorCheckable(Checkable):
    messages: List[AnalysisMessage]

    def analyze(self) -> Iterable[AnalysisMessage]:
        return self.messages

def run_checkables(checkables: Iterable[Checkable]) -> List[AnalysisMessage]:
    collector = MessageCollector()
    for checkable in checkables:
        collector.extend(checkable.analyze())
    return collector.get()

def analyze_any(
    entity: Union[types.ModuleType, type, FunctionInfo], options: AnalysisOptionSet
) -> Iterable[Checkable]:
    if inspect.isclass(entity):
        yield from analyze_class(cast(Type, entity), options)
    elif isinstance(entity, FunctionInfo):
        yield from analyze_function(entity, options)
    elif inspect.ismodule(entity):
        yield from analyze_module(cast(types.ModuleType, entity), options)
    else:
        raise CrosshairInternal("Entity type not analyzable: " + str(type(entity)))

def analyze_module(
    module: types.ModuleType, options: AnalysisOptionSet
) -> Iterable[Checkable]:
    """Analyze the classes and functions defined in a module."""
    module_name = module.__name__
    for name, member in inspect.getmembers(module):
        if not (
            inspect.isclass(member)
            or inspect.isfunction(member)
            or inspect.ismethod(member)
        ):
            continue
        if member.__module__ != module_name:
            # Modules often have contents that are imported from elsewhere
            continue
        if inspect.isclass(member):
            yield from analyze_class(member, options)
        else:
            yield from analyze_function(FunctionInfo.from_module(module, name), options)

def analyze_class(
    cls: type, options: AnalysisOptionSet = AnalysisOptionSet()
) -> Iterable[Checkable]:
    debug("Analyzing class ", cls.__name__)
    analysis_kinds = DEFAULT_OPTIONS.overlay(options).analysis_kind
    with condition_parser(analysis_kinds) as parser:
        class_conditions = parser.get_class_conditions(cls)
        for method_name, conditions in class_conditions.methods.items():
            if method_name == "__init__":
                # Don't check invariants on __init__.
                # (too often this just requires turning the invariant into a very
                # similar precondition)
                filtered_post = [
                    c
                    for c in conditions.post
                    if c.condition_type != ConditionExprType.INVARIANT
                ]
                conditions = replace(conditions, post=filtered_post)
            if conditions.has_any():
                # Note the use of getattr_static to check superclass contracts on
                # functions that the subclass doesn't define.
                ctxfn = FunctionInfo(
                    cls, method_name, inspect.getattr_static(cls, method_name)
                )
                for checkable in analyze_function(ctxfn, options=options):
                    yield ClampedCheckable(checkable, cls)

def analyze_function(
    ctxfn: Union[FunctionInfo, types.FunctionType, Callable],
    options: AnalysisOptionSet = AnalysisOptionSet(),
) -> List[Checkable]:

    if not isinstance(ctxfn, FunctionInfo):
        ctxfn = FunctionInfo.from_fn(ctxfn)
    debug("Analyzing ", ctxfn.name)
    pair = ctxfn.get_callable()
    fn_options = collect_options(pair[0]) if pair else AnalysisOptionSet()
    full_options = DEFAULT_OPTIONS.overlay(fn_options).overlay(options)
    if not full_options.enabled:
        debug("Skipping", ctxfn.name, " because CrossHair is not enabled")
        return []

    with condition_parser(full_options.analysis_kind) as parser:
        if not isinstance(ctxfn.context, type):
            conditions = parser.get_fn_conditions(ctxfn)
        else:
            class_conditions = parser.get_class_conditions(ctxfn.context)
            conditions = class_conditions.methods.get(ctxfn.name)

    if conditions is None:
        debug("Skipping", ctxfn.name, " because it has no conditions")
        return []
    syntax_messages = list(conditions.syntax_messages())
    if syntax_messages:
        messages = [
            AnalysisMessage(
                MessageType.SYNTAX_ERR,
                syntax_message.message,
                syntax_message.filename,
                syntax_message.line_num,
                0,
                "",
            )
            for syntax_message in syntax_messages
        ]
        return [SyntaxErrorCheckable(messages)]
    return [
        ConditionCheckable(
            ctxfn, full_options, replace(conditions, post=[post_condition])
        )
        for post_condition in conditions.post
        if post_condition.evaluate is not None
    ]

class ShortCircuitingContext:
    engaged = False

    def __enter__(self):
        assert not self.engaged
        self.engaged = True

    def __exit__(self, exc_type, exc_value, tb):
        assert self.engaged
        self.engaged = False
        return False

    def make_interceptor(self, original: Callable) -> Callable:
        # TODO: calling from_fn is wrong here
        subconditions = get_current_parser().get_fn_conditions(
            FunctionInfo.from_fn(original)
        )
        original_name = original.__name__
        if subconditions is None:
            return original
        sig = subconditions.sig

        def _crosshair_wrapper(*a: object, **kw: Dict[str, object]) -> object:
            space = optional_context_statespace()
            if (not self.engaged) or (not space) or space.running_framework_code:
                debug("Not short-circuiting", original_name, "(not engaged)")
                return original(*a, **kw)

            with NoTracing():
                bound = sig.bind(*a, **kw)
                assert subconditions is not None
                return_type = consider_shortcircuit(original, sig, bound, subconditions)
                if return_type is None:
                    callinto_probability = 1.0
                else:
                    short_stats, callinto_stats = space.stats_lookahead()
                    if callinto_stats.unknown_pct < short_stats.unknown_pct:
                        callinto_probability = 1.0
                    else:
                        callinto_probability = 0.7

                debug("short circuit: call-into probability", callinto_probability)
                do_short_circuit = space.fork_parallel(
                    callinto_probability, desc=f"shortcircuit {original_name}"
                )
                # Statespace can pick either even with 0.0 or 1.0 probability:
                do_short_circuit &= return_type is not None
            if do_short_circuit:
                assert return_type is not None
                try:
                    self.engaged = False
                    debug(
                        "short circuit: Short circuiting over a call to ", original_name
                    )
                    return shortcircuit(original, sig, bound, return_type)
                finally:
                    self.engaged = True
            else:
                debug("short circuit: Not short circuiting", original_name)
                return original(*a, **kw)

        functools.update_wrapper(_crosshair_wrapper, original)
        return _crosshair_wrapper

@dataclass
class CallTreeAnalysis:
    messages: Sequence[AnalysisMessage]
    verification_status: VerificationStatus
    num_confirmed_paths: int = 0

def analyze_calltree(
    options: AnalysisOptions, conditions: Conditions
) -> CallTreeAnalysis:
    fn = conditions.fn
    debug("Begin analyze calltree ", fn.__name__)

    all_messages = MessageCollector()
    search_root = SinglePathNode(True)
    space_exhausted = False
    failing_precondition: Optional[ConditionExpr] = (
        conditions.pre[0] if conditions.pre else None
    )
    failing_precondition_reason: str = ""
    num_confirmed_paths = 0

    _ = get_subclass_map()  # ensure loaded
    short_circuit = ShortCircuitingContext()
    top_analysis: Optional[CallAnalysis] = None
    enforced_conditions = EnforcedConditions(
        interceptor=short_circuit.make_interceptor,
    )
    patched = Patched()
    # TODO clean up how encofrced conditions works here?
    with enforced_conditions, patched:
        for i in range(1, options.max_iterations + 1):
            start = time.monotonic()
            if start > options.deadline:
                debug("Exceeded condition timeout, stopping")
                break
            options.incr("num_paths")
            debug("Iteration ", i)
            space = StateSpace(
                execution_deadline=start + options.per_path_timeout,
                model_check_timeout=options.per_path_timeout / 2,
                search_root=search_root,
            )
            try:
                with StateSpaceContext(space), COMPOSITE_TRACER:
                    # The real work happens here!:
                    call_analysis = attempt_call(
                        conditions, fn, short_circuit, enforced_conditions
                    )
                if failing_precondition is not None:
                    cur_precondition = call_analysis.failing_precondition
                    if cur_precondition is None:
                        if call_analysis.verification_status is not None:
                            # We escaped the all the pre conditions on this try:
                            failing_precondition = None
                    elif (
                        cur_precondition.line == failing_precondition.line
                        and call_analysis.failing_precondition_reason
                    ):
                        failing_precondition_reason = (
                            call_analysis.failing_precondition_reason
                        )
                    elif cur_precondition.line > failing_precondition.line:
                        failing_precondition = cur_precondition
                        failing_precondition_reason = (
                            call_analysis.failing_precondition_reason
                        )

            except UnexploredPath:
                call_analysis = CallAnalysis(VerificationStatus.UNKNOWN)
            except IgnoreAttempt:
                call_analysis = CallAnalysis()
            status = call_analysis.verification_status
            if status == VerificationStatus.CONFIRMED:
                num_confirmed_paths += 1
            top_analysis, space_exhausted = space.bubble_status(call_analysis)
            debug("Path tree stats", search_root.stats())
            overall_status = top_analysis.verification_status if top_analysis else None
            debug(
                "Iter complete. Worst status found so far:",
                overall_status.name if overall_status else "None",
            )
            if space_exhausted or top_analysis == VerificationStatus.REFUTED:
                break
    top_analysis = search_root.child.get_result()
    if top_analysis.messages:
        all_messages.extend(
            replace(
                m, test_fn=fn.__qualname__, condition_src=conditions.post[0].expr_source
            )
            for m in top_analysis.messages
        )
    if top_analysis.verification_status is None:
        top_analysis.verification_status = VerificationStatus.UNKNOWN
    if failing_precondition:
        assert num_confirmed_paths == 0
        message = f"Unable to meet precondition"
        if failing_precondition_reason:
            message += f" (possibly because {failing_precondition_reason}?)"
        all_messages.extend(
            [
                AnalysisMessage(
                    MessageType.PRE_UNSAT,
                    message + ".",
                    failing_precondition.filename,
                    failing_precondition.line,
                    0,
                    "",
                )
            ]
        )
        top_analysis = CallAnalysis(VerificationStatus.REFUTED)

    assert top_analysis.verification_status is not None
    debug(
        ("Exhausted" if space_exhausted else "Aborted"),
        "calltree search with",
        top_analysis.verification_status.name,
        "and",
        len(all_messages.get()),
        "messages.",
        "Number of iterations: ",
        i,
    )
    return CallTreeAnalysis(
        messages=all_messages.get(),
        verification_status=top_analysis.verification_status,
        num_confirmed_paths=num_confirmed_paths,
    )

class UnEqual:
    pass

_UNEQUAL = UnEqual()

def deep_eq(old_val: object, new_val: object, visiting: Set[Tuple[int, int]]) -> bool:
    # TODO: test just about all of this
    if old_val is new_val:
        return True
    if type(old_val) != type(new_val):
        return False
    visit_key = (id(old_val), id(new_val))
    if visit_key in visiting:
        return True
    visiting.add(visit_key)
    try:
        with NoTracing():
            is_ch_value = isinstance(old_val, CrossHairValue)
        if is_ch_value:
            return old_val == new_val
        elif hasattr(old_val, "__dict__") and hasattr(new_val, "__dict__"):
            return deep_eq(old_val.__dict__, new_val.__dict__, visiting)
        elif isinstance(old_val, dict):
            assert isinstance(new_val, dict)
            for key in set(itertools.chain(old_val.keys(), *new_val.keys())):
                if (key in old_val) ^ (key in new_val):
                    return False
                if not deep_eq(
                    old_val.get(key, _UNEQUAL), new_val.get(key, _UNEQUAL), visiting
                ):
                    return False
            return True
        elif isinstance(old_val, Iterable):
            assert isinstance(new_val, Sized)
            if isinstance(old_val, Sized):
                if len(old_val) != len(new_val):
                    return False
            assert isinstance(new_val, Iterable)
            return all(
                deep_eq(o, n, visiting)
                for (o, n) in itertools.zip_longest(
                    old_val, new_val, fillvalue=_UNEQUAL
                )
            )
        elif type(old_val) is object:
            # Plain object instances are close enough to equal for our purposes
            return True
        else:
            # hopefully this is just ints, bools, etc
            return old_val == new_val
    finally:
        visiting.remove(visit_key)

class MessageGenerator:
    def __init__(self, fn: Callable):
        self.filename = ""
        if hasattr(fn, "__code__"):
            code_obj = fn.__code__
            self.filename = code_obj.co_filename
            self.start_lineno = code_obj.co_firstlineno
            _, _, lines = sourcelines(fn)
            self.end_lineno = self.start_lineno + len(lines)

    def make(
        self,
        message_type: MessageType,
        detail: str,
        suggested_filename: Optional[str],
        suggested_lineno: int,
        tb: str,
    ) -> AnalysisMessage:
        if (
            suggested_filename is not None
            and (os.path.abspath(suggested_filename) == os.path.abspath(self.filename))
            and (self.start_lineno <= suggested_lineno <= self.end_lineno)
        ):
            return AnalysisMessage(
                message_type, detail, suggested_filename, suggested_lineno, 0, tb
            )
        else:
            exprline = "<unknown>"
            if suggested_filename is not None:
                lines = linecache.getlines(suggested_filename)
                try:
                    exprline = lines[suggested_lineno - 1].strip()
                except IndexError:
                    pass
            detail = f'"{exprline}" yields {detail}'
            return AnalysisMessage(
                message_type, detail, self.filename, self.start_lineno, 0, tb
            )

def attempt_call(
    conditions: Conditions,
    fn: Callable,
    short_circuit: ShortCircuitingContext,
    enforced_conditions: EnforcedConditions,
    bound_args: Optional[BoundArguments] = None,
) -> CallAnalysis:
    assert fn is conditions.fn  # TODO: eliminate the explicit `fn` parameter?
    space = context_statespace()
    msg_gen = MessageGenerator(conditions.src_fn)
    with enforced_conditions.enabled_enforcement(), NoTracing():
        bound_args = gen_args(conditions.sig) if bound_args is None else bound_args

        # TODO: looks wrong(-ish) to guard this with NoTracing().
        # Copy on custom objects may require patched builtins. (datetime.timedelta is one such case)
        original_args = copy.deepcopy(bound_args)
    space.checkpoint()

    lcls: Mapping[str, object] = bound_args.arguments
    # In preconditions, __old__ exists but is just bound to the same args.
    # This lets people write class invariants using `__old__` to, for example,
    # demonstrate immutability.
    lcls = {"__old__": AttributeHolder(lcls), **lcls}
    expected_exceptions = conditions.raises
    for precondition in conditions.pre:
        if not precondition.evaluate:
            continue
        with ExceptionFilter(expected_exceptions) as efilter:
            with enforced_conditions.enabled_enforcement(), short_circuit:
                precondition_ok = prefer_true(precondition.evaluate(lcls))
            if not precondition_ok:
                debug("Failed to meet precondition", precondition.expr_source)
                return CallAnalysis(failing_precondition=precondition)
        if efilter.ignore:
            debug("Ignored exception in precondition.", efilter.analysis)
            return efilter.analysis
        elif efilter.user_exc is not None:
            (user_exc, tb) = efilter.user_exc
            debug(
                "Exception attempting to meet precondition",
                precondition.expr_source,
                ":",
                user_exc,
                tb.format(),
            )
            return CallAnalysis(
                failing_precondition=precondition,
                failing_precondition_reason=f'it raised "{repr(user_exc)} at {tb.format()[-1]}"',
            )

    with ExceptionFilter(expected_exceptions) as efilter:
        with enforced_conditions.enabled_enforcement(), short_circuit:
            assert not space.running_framework_code
            debug("Starting function body")
            __return__ = NoEnforce(fn)(*bound_args.args, **bound_args.kwargs)
        lcls = {
            **bound_args.arguments,
            "__return__": __return__,
            "_": __return__,
            "__old__": AttributeHolder(original_args.arguments),
            fn.__name__: fn,
        }

    if efilter.ignore:
        debug("Ignored exception in function.", efilter.analysis)
        return efilter.analysis
    elif efilter.user_exc is not None:
        (e, tb) = efilter.user_exc
        space.detach_path(e)
        detail = name_of_type(type(e)) + ": " + str(e)
        frame_filename, frame_lineno = frame_summary_for_fn(conditions.src_fn, tb)
        tb_desc = tb.format()
        detail += " " + conditions.format_counterexample(original_args)
        debug("exception while evaluating function body:", detail, tb_desc)
        return CallAnalysis(
            VerificationStatus.REFUTED,
            [
                msg_gen.make(
                    MessageType.EXEC_ERR,
                    detail,
                    frame_filename,
                    frame_lineno,
                    "".join(tb_desc),
                )
            ],
        )

    for argname, argval in bound_args.arguments.items():
        if (
            conditions.mutable_args is not None
            and argname not in conditions.mutable_args
        ):
            old_val, new_val = original_args.arguments[argname], argval
            # TODO: Do we really need custom equality here? Would love to drop that
            # `deep_eq` function.
            if not deep_eq(old_val, new_val, set()):
                space.detach_path()
                detail = 'Argument "{}" is not marked as mutable, but changed from {} to {}'.format(
                    argname, old_val, new_val
                )
                debug("Mutablity problem:", detail)
                return CallAnalysis(
                    VerificationStatus.REFUTED,
                    [msg_gen.make(MessageType.POST_ERR, detail, None, 0, "")],
                )

    (post_condition,) = conditions.post
    assert post_condition.evaluate is not None
    with ExceptionFilter(expected_exceptions) as efilter:
        # TODO: re-enable post-condition short circuiting. This will require refactoring how
        # enforced conditions and short curcuiting interact, so that post-conditions are
        # selectively run when, and only when, performing a short circuit.
        # with enforced_conditions.enabled_enforcement(), short_circuit:
        assert not space.running_framework_code
        debug("Starting postcondition")
        isok = bool(post_condition.evaluate(lcls))
    if efilter.ignore:
        debug("Ignored exception in postcondition.", efilter.analysis)
        return efilter.analysis
    elif efilter.user_exc is not None:
        (e, tb) = efilter.user_exc
        space.detach_path(e)
        detail = (
            repr(e) + " " + conditions.format_counterexample(original_args, __return__)
        )
        debug("exception while calling postcondition:", detail)
        debug("exception traceback:", test_stack(tb))
        failures = [
            msg_gen.make(
                MessageType.POST_ERR,
                detail,
                post_condition.filename,
                post_condition.line,
                "".join(tb.format()),
            )
        ]
        return CallAnalysis(VerificationStatus.REFUTED, failures)
    if isok:
        debug("Postcondition confirmed.")
        return CallAnalysis(VerificationStatus.CONFIRMED)
    else:
        space.detach_path()
        detail = "false " + conditions.format_counterexample(original_args, __return__)
        debug(detail)
        failures = [
            msg_gen.make(
                MessageType.POST_FAIL,
                detail,
                post_condition.filename,
                post_condition.line,
                "",
            )
        ]
        return CallAnalysis(VerificationStatus.REFUTED, failures)

# Objects of these types are known to always be *deeply* immutable:
_ATOMIC_IMMUTABLE_TYPES = (
    type(None),
    int,
    str,
    float,
    complex,
    types.FunctionType,
    types.BuiltinFunctionType,
    types.LambdaType,
    types.MethodType,
    types.BuiltinMethodType,
)

def _mutability_testing_hash(o: object) -> int:
    if isinstance(o, _ATOMIC_IMMUTABLE_TYPES):
        return 0
    if hasattr(o, "__ch_is_deeply_immutable__"):
        if o.__ch_is_deeply_immutable__():  # type: ignore
            return 0
        else:
            raise TypeError
    typ = type(o)
    if not hasattr(typ, "__hash__"):
        raise TypeError
    # We err on the side of mutability if this object is using the default hash:
    if typ.__hash__ is object.__hash__:
        raise TypeError
    return typ.__hash__(o)

def is_deeply_immutable(o: object) -> bool:
    with TracingOnly(PatchingModule({hash: _mutability_testing_hash})):
        # debug('entered patching context', COMPOSITE_TRACER.modules)
        try:
            hash(o)
            return True
        except TypeError:
            return False

def consider_shortcircuit(
    fn: Callable, sig: Signature, bound: BoundArguments, subconditions: Conditions
) -> Optional[type]:
    """
    Consider the feasibility of short-circuiting (skipping) a function with the given arguments.

    :return: The type of a symbolic value that could be returned by ``fn``.
    :return: None if a short-circuiting should not be attempted.
    """
    return_type = sig.return_annotation

    mutable_args = subconditions.mutable_args
    if mutable_args is None or len(mutable_args) > 0:
        # we don't deal with mutation inside the skipped function yet.
        debug("aborting shortcircuit: function has matuable args")
        return None

    # Deduce type vars if necessary
    if len(typing_inspect.get_parameters(return_type)) > 0 or typing_inspect.is_typevar(
        return_type
    ):

        typevar_bindings: typing.ChainMap[object, type] = collections.ChainMap()
        bound.apply_defaults()
        for param in sig.parameters.values():
            argval = bound.arguments[param.name]
            # We don't need all args to be symbolic, but we don't currently
            # short circuit in that case as a heuristic.
            if not isinstance(argval, CrossHairValue):
                debug("aborting shortcircuit:", param.name, "is not symbolic")
                return None
            value_type = python_type(argval)
            if not dynamic_typing.unify(value_type, param.annotation, typevar_bindings):
                debug("aborting shortcircuit", param.name, "fails unification")
                return None
        return_type = dynamic_typing.realize(sig.return_annotation, typevar_bindings)
    return return_type

def shortcircuit(
    fn: Callable, sig: Signature, bound: BoundArguments, return_type: Type
) -> object:
    space = context_statespace()
    debug("short circuit: Deduced return type was ", return_type)

    # Deep copy the arguments for reconciliation later.
    # (we know that this function won't mutate them, but not that others won't)
    argscopy = {}
    for name, val in bound.arguments.items():
        if is_deeply_immutable(val):
            argscopy[name] = val
        else:
            with NoTracing():  # TODO: decide how deep copies should work
                argscopy[name] = copy.deepcopy(val)
    bound_copy = BoundArguments(sig, argscopy)  # type: ignore

    retval = None
    if return_type is not type(None):
        # note that the enforcement wrapper ensures postconditions for us, so
        # we can just return a free variable here.
        retval = proxy_for_type(return_type, "proxyreturn" + space.uniq())

    def reconciled() -> bool:
        return retval == fn(*bound_copy.args, **bound_copy.kwargs)

    space.defer_assumption("Reconcile short circuit", reconciled)

    return retval