public class Cache<T,R> {
    public static <T,R> Function<T,R> memoize(Function<T,R> src) {
        final Map<T, R> data = new HashMap<>();
        return key -> {
            R res = data.get(key);
            if(res == null) {
                res = src.apply(key);
                data.put(key, res);
            }
            return res;
        };
    }
} public class Countify {

    static class Counter<T, R> implements ICounter<T, R> {
        private int count=0;
        private final Function<T, R> inner;

        public Counter(Function<T, R> inner) {
            this.inner = inner;
        }

        @Override
        public int getCount() {
            return count;
        }

        @Override
        public void reset() {
            count =0;
        }

        @Override
        public R apply(T arg) {
            count++;
            return inner.apply(arg);
        }
    }

    public static <T,R> ICounter of(Function<T,R> inner) {
        return new Counter<>(inner);
    }
} public class FileRequest implements IRequest{
    @Override
    public Iterable<String> getContent(String path) {
        String[] parts = path.split("/");
        path = parts[parts.length-1]
                .replace('?', '-')
                .replace('&', '-')
                .replace('=', '-')
                .replace(',', '-')
                .substring(0,68);
        System.out.println(path);
        ArrayList<String> res = new ArrayList<>();
        try(InputStream in = ClassLoader.getSystemResource(path).openStream()) {
            /*
             * Consumir o Inputstream e adicionar dados ao res
             */
            try(BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    res.add(line);
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return res;
    }
} public interface ICounter<T,R> extends Function<T,R> {
    int getCount();

    void reset();
} public class WeatherService implements AutoCloseable{

    private final WeatherWebApi api;

    public WeatherService(WeatherWebApi api) {
        this.api = api;
    }

    public WeatherService() {
        api = new WeatherWebApi(new HttpRequest());
    }

    public CompletableFuture<Stream<Location>> search(String query) {
        return api.search(query)
                .thenApply(str -> str
                    .map(this::dtoToLocation));
    }

    protected Location dtoToLocation(LocationDto loc) {
        return new Location(
                loc.getCountry(),
                loc.getRegion(),
                loc.getLatitude(),
                loc.getLongitude(),
                last30daysWeather(loc.getLatitude(), loc.getLongitude())::join,
                (from, to) -> pastWeather(loc.getLatitude(), loc.getLongitude(), from, to).join());
    }

    public CompletableFuture<Stream<WeatherInfo>> last30daysWeather(double lat, double log) {
        return this.pastWeather(lat, log, now().minusDays(30), now().minusDays(1));
    }

    private static WeatherInfo dtoToWeatherInfo(WeatherInfoDto dto) {
        return new WeatherInfo(
                dto.getDate(),
                dto.getTempC(),
                dto.getDescription(),
                dto.getPrecipMM(),
                dto.getFeelsLikeC());
    }

    public CompletableFuture<Stream<WeatherInfo>> pastWeather(double lat, double log, LocalDate from, LocalDate to) {
        return api
                .pastWeather(lat, log, from, to)
                .thenApply(str -> str
                    .map(dto -> dtoToWeatherInfo(dto)));
    }

    @Override
    public void close() throws Exception {
        api.close();
    }
} public class WeatherServiceCache extends WeatherService{

    public WeatherServiceCache(WeatherWebApi api) {
        super(api);
    }

    private final Map<Coords, Map<LocalDate, WeatherInfo>> cache = new HashMap<>();

    @Override
    protected Location dtoToLocation(LocationDto loc) {
        return new Location(
                loc.getCountry(),
                loc.getRegion(),
                loc.getLatitude(),
                loc.getLongitude(),
                last30daysWeatherSupplier(loc),
                (from, to) -> pastWeather(loc.getLatitude(), loc.getLongitude(), from, to).join());
    }

    private Supplier<Stream<WeatherInfo>> last30daysWeatherSupplier(LocationDto loc) {
        /**
         * Immediately dispatch request for last 30 days weather asynchronously.
         */
        CompletableFuture<Stream<WeatherInfo>> promise =
                last30daysWeather(loc.getLatitude(), loc.getLongitude());
        boolean[] firstReq = { true };
        /**
         * The supplier returns the result of the promise on first get.
         * Further invocations will call last30daysWeather() again which in turn
         * calls the pastWeather() of this WeatherServiceCache, which gets past
         * weather from cache.
         */
        return () -> {
            Stream<WeatherInfo> res = firstReq[0]
                    ? promise.join()
                    : last30daysWeather(loc.getLatitude(), loc.getLongitude()).join(); // ALREADY on cache.
            firstReq[0] = false;
            return res;
        };
    }

    @Override
    public CompletableFuture<Stream<WeatherInfo>> pastWeather(double lat, double log, LocalDate from, LocalDate to) {
        Coords location = new Coords(lat, log);
        Map<LocalDate, WeatherInfo> past = getOrCreate(location, cache);
        List<LocalDate> keys = Stream
                .iterate(from, prev -> prev.plusDays(1))
                .limit(ChronoUnit.DAYS.between(from, to.plusDays(1)))
                .collect(toList());
        if (past.keySet().containsAll(keys)) {
            CompletableFuture<Stream<WeatherInfo>> res = new CompletableFuture<>();
            res.complete(keys.stream().map(past::get));
            return res;
        }
        CompletableFuture<Stream<WeatherInfo>> values = super.pastWeather(lat, log, from, to);
        Iterator<LocalDate> keysIter = keys.iterator();
        return values.thenApply(vs -> vs
                .peek(item -> past.put(keysIter.next(), item)));
    }

    private static Map<LocalDate, WeatherInfo> getOrCreate(
            Coords coords,
            Map<Coords, Map<LocalDate, WeatherInfo>> cache)
    {
        Map<LocalDate, WeatherInfo> weathers = cache.get(coords);
        if(weathers == null) {
            weathers = new HashMap<>();
            cache.put(coords, weathers);
        }
        return weathers;
    }
} public class WeatherWebApi {

    private static final String WEATHER_TOKEN;
    private static final String WEATHER_HOST = "http://api.worldweatheronline.com";
    private static final String WEATHER_PAST = "/premium/v1/past-weather.ashx";
    private static final String WEATHER_PAST_ARGS =
            "?q=%s&date=%s&enddate=%s&tp=24&format=csv&key=%s";
    private static final String WEATHER_SEARCH="/premium/v1/search.ashx?query=%s";
    private static final String WEATHER_SEARCH_ARGS="&format=tab&key=%s";

    static {
        try {
            URL keyFile = ClassLoader.getSystemResource("worldweatheronline-app-key.txt");
            if(keyFile == null) {
               throw new IllegalStateException(
                       "YOU MUST GOT a KEY in developer.worldweatheronline.com and place it in src/main/resources/worldweatheronline-app-key.txt");
            } else {
                InputStream keyStream = keyFile.openStream();
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(keyStream))) {
                    WEATHER_TOKEN = reader.readLine();
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private final IRequest req;

    public WeatherWebApi(IRequest req) {
        this.req = req;
    }

    /**
     * E.g. http://api.worldweatheronline.com/free/v2/search.ashx?query=oporto&format=tab&key=*****
     */

    public Iterable<LocationDto> search(String query) {
        String path = WEATHER_HOST + WEATHER_SEARCH + WEATHER_SEARCH_ARGS;
        String url = String.format(path, query, WEATHER_TOKEN);
        Iterable<String> content = () -> req.getContent(url).iterator();
        Iterable<String> iterable =	filter(content, (String s) -> !s.startsWith("#"));
        return map(iterable, LocationDto::valueOf);
    }

    /**
     * E.g. http://api.worldweatheronline.com/free/v2/search.ashx?query=oporto&format=tab&key=*****
     */
    public Iterable<WeatherInfoDto> pastWeather(
            double lat,
            double log,
            LocalDate from,
            LocalDate to
    ) {
        String query = lat + "," + log;
        String path = WEATHER_HOST + WEATHER_PAST +
                String.format(WEATHER_PAST_ARGS, query, from, to, WEATHER_TOKEN);
        Iterable<String> content = () -> req.getContent(path).iterator();
        Iterable<String> stringIterable = filter(content, s->!s.startsWith("#"));
        Iterable<String>filtered = skip(stringIterable,1);	//  Skip line: Not Available
        int[] counter = {0};
        Predicate<String> isEvenLine = item -> ++counter[0] % 2==0;
        filtered = filter(filtered,isEvenLine);//even lines filter
        return map(filtered, WeatherInfoDto::valueOf); //to weatherinfo objects
    }
} public class Location {
    private final String country;
    private final String region;
    private final double latitude;
    private final double longitude;
    private final Iterable<WeatherInfo> last30daysWeather;
    private final BiFunction<LocalDate, LocalDate, Iterable<WeatherInfo>> pastWeather;

    public Location(String country, String region, double latitude, double longitude, Iterable<WeatherInfo> last30daysWeather, BiFunction<LocalDate, LocalDate, Iterable<WeatherInfo>> pastWeather) {
        this.country = country;
        this.region = region;
        this.latitude = latitude;
        this.longitude = longitude;
        this.last30daysWeather = last30daysWeather;
        this.pastWeather = pastWeather;
    }

    public String getCountry() {
        return country;
    }

    public String getRegion() {
        return region;
    }

    public double getLatitude() {
        return latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public Iterable<WeatherInfo> getLast30daysWeather() {
        return last30daysWeather;
    }

    public Iterable<WeatherInfo> getPastWeather(LocalDate from, LocalDate to) {
        return pastWeather.apply(from, to);
    }

    @Override
    public String toString() {
        return "Location{" +
                "country='" + country + '\'' +
                ", region='" + region + '\'' +
                ", latitude=" + latitude +
                ", longitude=" + longitude +
                ", last30daysWeather=" + last30daysWeather +
                '}';
    }
} public class WeatherInfo {
    private final LocalDate date;
    private final int tempC;
    private final String description;
    private final double  precipMM;
    private final int feelsLikeC;

    public WeatherInfo(LocalDate date, int tempC, String description, double precipMM, int feelsLikeC) {
        this.date = date;
        this.tempC = tempC;
        this.description = description;
        this.precipMM = precipMM;
        this.feelsLikeC = feelsLikeC;
    }

    public LocalDate getDate() {
        return date;
    }

    public int getTempC() {
        return tempC;
    }

    public String getDescription() {
        return description;
    }

    public double getPrecipMM() {
        return precipMM;
    }

    public int getFeelsLikeC() {
        return feelsLikeC;
    }

    @Override
    public String toString() {
        return "WeatherInfo{" +
                "date=" + date +
                ", tempC=" + tempC +
                ", description='" + description + '\'' +
                ", precipMM=" + precipMM +
                ", feelsLikeC=" + feelsLikeC +
                '}';
    }
} public static <T> int count(Iterable<T> data) {
    int size = 0;
    for (T item: data) {
        size++;
    }
    return size;
} public static <T> Iterable<T> distinct(Iterable<T> data) {
    return null;
} public static <T> Iterable<T> filter(Iterable<T> data, Predicate<T> p) {
    return () -> new IteratorFilter(data.iterator(), p);
} public static <T, R> Iterable<R> map(Iterable<T> data, Function<T, R> mapper) {

    return null;
} public static <T> Iterable<T> skip(Iterable<T> data, int nr) {
    return () -> {
        Iterator<T> iter = data.iterator();
        for (int i = 0; i < nr && iter.hasNext(); i++) iter.next();
        return iter;
    };
}
import org.junit.Test;
import util.Cache;
import util.Countify;
import util.FileRequest;
import util.ICounter;
import weather.WeatherService;
import weather.WeatherServiceCache;
import weather.data.WeatherWebApi;
import weather.model.Location;
import weather.model.WeatherInfo;
import java.time.LocalDate;
import java.util.function.Function;
import static java.lang.System.out;
import static java.time.LocalDate.of;
import static org.junit.Assert.assertEquals;
import static util.queries.LazyQueries.count;
import static util.queries.LazyQueries.distinct;
import static util.queries.LazyQueries.filter;
import static util.queries.LazyQueries.map;
import static util.queries.LazyQueries.skip;
/*
 * Copyright (c) 2017, Miguel Gamboa
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */




/**
 * @author Miguel Gamboa
 *         created on 29-03-2017
 */
public class WeatherDomainTest {
    @Test
    public void testWeatherServiceLazy(){
        /**
         * Arrange WeatherService --> WeatherWebApi --> Countify --> FileRequest
         */
        ICounter<String, Iterable<String>> req = Countify.of(new FileRequest()::getContent);
        WeatherService api = new WeatherService(new WeatherWebApi(req::apply));
        /**
         * Act and Assert
         * Counts 0 request while iterator() is not consumed
         */
        Iterable<Location> locals = api.search("Porto");
        assertEquals(0, req.getCount());
        locals = filter(locals, l -> l.getLatitude() > 0 ); // Iterable<T> ---> Iterable<T>
        assertEquals(0, req.getCount());
        /**
         * Counts 1 request when iterates to get the first Location
         */
        Location loc = locals.iterator().next();
        assertEquals(1, req.getCount());
        Iterable<WeatherInfo> infos = loc.getPastWeather(of(2017,02,01), of(2017,02,28));
        assertEquals(1, req.getCount());
        infos = filter(infos, info -> info.getDescription().toLowerCase().contains("sun"));
        Iterable<Integer> temps = map(infos, WeatherInfo::getTempC);
        temps = distinct(temps);
        assertEquals(1, req.getCount());
        /**
         * When we iterate over the pastWeather then we make one more request
         */
        assertEquals(5, count(temps)); // iterates all items
        assertEquals(2, req.getCount());
        assertEquals((long) 21, (long) skip(temps, 2).iterator().next()); // another iterator